Skip to content

feat: add signature verification for Kubernetes profiles#737

Open
matthyx wants to merge 20 commits intomainfrom
sign-ap
Open

feat: add signature verification for Kubernetes profiles#737
matthyx wants to merge 20 commits intomainfrom
sign-ap

Conversation

@matthyx
Copy link
Contributor

@matthyx matthyx commented Mar 3, 2026

Implement generic signing and verification framework for ApplicationProfiles and SeccompProfiles using ECDSA P-256 signatures compatible with cosign.

Key features:

  • SignableProfile interface for extensible profile type support
  • Keyless signing support using OIDC identity providers
  • Key-based signing for offline/air-gapped environments
  • Signature storage in profile metadata.annotations
  • CLI tool (sign-profile) for signing and verifying profiles
  • Automatic verification in ApplicationProfileCache on profile load

Core implementation:

  • pkg/signature/ package with ECDSA P-256 signing/verification
  • Profile adapters for ApplicationProfile and SeccompProfile
  • EnableProfileVerification config flag (default: false)
  • On verification failure: skip profile with warning (don't crash)

Files:

  • pkg/signature/annotations.go, interface.go, sign.go, verify.go
  • pkg/signature/cosign_adapter.go, signer.go, verifier.go
  • pkg/signature/profiles/applicationprofile_adapter.go
  • pkg/signature/profiles/seccompprofile_adapter.go
  • cmd/sign-profile/main.go
  • pkg/config/config.go - add EnableProfileVerification flag
  • pkg/objectcache/applicationprofilecache/ - integrate verification at load time
  • docs/signing/README.md - comprehensive documentation

Tests:

  • 15 signature package tests covering signing, verification, tampering detection
  • 5 adapter tests for ApplicationProfile and SeccompProfile adapters
  • All tests passing

Overview

Summary by CodeRabbit

  • New Features

    • Object signing and verification for ApplicationProfiles, SeccompProfiles, NetworkNeighborhoods, and Rules (key-based and keyless/OIDC).
    • CLI to sign, verify, generate keys, and extract signature metadata.
  • Behavior

    • Optional signature verification during load/merge and rule sync (config/env flag ENABLE_SIGNATURE_VERIFICATION, default: disabled); items with missing/invalid signatures are skipped and recorded.
  • Documentation

    • Comprehensive signing/verification guide and CLI reference added.
  • Tests

    • Extensive unit and integration tests covering signing, verification, adapters, and tamper detection.

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Cosign-based signing and verification subsystem: signable adapters for profiles, rules, and network neighborhoods; CLI and docs for signing; config toggle to enable verification; verification integrated into caches and RulesWatcher; many tests and dependency updates.

Changes

Cohort / File(s) Summary
Signature core
pkg/signature/interface.go, pkg/signature/annotations.go, pkg/signature/cosign_adapter.go, pkg/signature/signer.go, pkg/signature/verifier.go, pkg/signature/sign.go, pkg/signature/verify.go
New signing/verification API, annotation constants, Cosign adapter (keyless/keyed signing, hashing, annotation encode/decode), Signer/Verifier implementations, and top-level sign/verify helpers.
Signature tests
pkg/signature/*_test.go, pkg/signature/cluster_flow_test.go, pkg/signature/cluster_scenario_test.go, pkg/signature/verify_test.go
Extensive unit/integration tests covering signing, verification, tamper detection, certificate handling, and cluster-style scenarios.
Profile adapters & tests
pkg/signature/profiles/applicationprofile_adapter.go, pkg/signature/profiles/seccompprofile_adapter.go, pkg/signature/profiles/networkneighborhood_adapter.go, pkg/signature/profiles/rules_adapter.go, pkg/signature/profiles/*_test.go
Adapters exposing ApplicationProfile, SeccompProfile, NetworkNeighborhood, and Rules as SignableObject with normalized content; tests validate adapters and sign/verify flows, including empty TypeMeta fallbacks.
Cache & watcher integration
pkg/objectcache/.../applicationprofilecache.go, pkg/objectcache/.../networkneighborhoodcache.go, pkg/rulemanager/ruleswatcher/watcher.go, cmd/main.go
Integrates signature verification into profile and network neighborhood cache flows and RulesWatcher. Verification is conditional on Config.EnableSignatureVerification. NewRulesWatcher constructor now accepts cfg *config.Config.
Configuration
pkg/config/config.go
Added EnableSignatureVerification bool (env: ENABLE_SIGNATURE_VERIFICATION, default false).
CLI tool
cmd/sign-object/main.go
New CLI to sign, verify, generate keypairs, and extract signatures with object-type detection and keyless/keyed options.
Documentation
docs/signing/README.md
Comprehensive signing/verification docs, annotation schema, CLI reference, workflows, threat model, and troubleshooting.
Dependencies & toolchain
go.mod
Go toolchain bumped and numerous dependency upgrades/additions (sigstore/cosign, fulcio, rekor, k8s libs, OpenTelemetry, etc.).

Sequence Diagram(s)

sequenceDiagram
    participant K8S as Kubernetes API
    participant CACHE as Cache/Watcher
    participant VERIFY as Signature Verifier
    participant COSIGN as CosignAdapter

    K8S->>CACHE: Fetch object
    CACHE->>VERIFY: Request verification (if cfg.EnableSignatureVerification)
    VERIFY->>COSIGN: Compute canonical content hash
    COSIGN-->>VERIFY: ContentHash
    VERIFY->>COSIGN: Decode signature from annotations
    COSIGN-->>VERIFY: Signature + Certificate
    VERIFY->>COSIGN: Verify signature against hash
    alt verification succeeds
        VERIFY-->>CACHE: Mark object verified
        CACHE->>K8S: Apply/store object
    else verification fails
        VERIFY-->>CACHE: Report failure / increment skipped metric
        CACHE->>CACHE: Skip update / record error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled bytes and chased each hash,

I stitched small signatures in a flash.
Keyless breeze or private key snug,
I left a pawprint, a tiny hug.
Trust now hops along each guarded path.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature addition: signature verification for Kubernetes profiles, which aligns with the core objective of implementing signing and verification framework.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sign-ap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (4)
pkg/signature/sign_test.go (1)

170-175: Test case name is misleading - profile has empty annotations, not nil.

The test case "No annotations" uses NewMockSignableProfile which initializes annotations: make(map[string]string). This creates an empty map, not nil. The test likely passes because DecodeSignatureFromAnnotations fails on an empty map, but the test name suggests it's testing the nil check path.

Consider either:

  1. Renaming to "Empty annotations", or
  2. Using &MockSignableProfile{annotations: nil, ...} to truly test the nil case
📝 Suggested clarification
 		{
-			name:      "No annotations",
-			profile:   NewMockSignableProfile("uid", "ns", "name", map[string]string{}),
+			name:      "Nil annotations",
+			profile:   &MockSignableProfile{uid: "uid", namespace: "ns", name: "name", content: map[string]string{}, annotations: nil},
 			wantErr:   true,
 			setupSign: false,
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/sign_test.go` around lines 170 - 175, The test case name "No
annotations" is misleading because NewMockSignableProfile creates an empty map
(annotations: make(map[string]string)); update the test to either rename the
case to "Empty annotations" to reflect the actual input, or change the fixture
to construct a MockSignableProfile with annotations set to nil (e.g.,
&MockSignableProfile{annotations: nil, ...}) so it exercises the nil-path in
DecodeSignatureFromAnnotations; modify the entry that currently calls
NewMockSignableProfile("uid","ns","name", map[string]string{}) to your chosen
fix and update the test name accordingly.
pkg/signature/verify.go (1)

29-49: Consider adding a comment explaining why useKeyless: true is hardcoded for verification.

Both NewCosignAdapter(true) and NewCosignVerifier(true) use hardcoded true. This works because the verification uses the certificate stored in the annotations (from either keyless or key-based signing), but the reasoning isn't immediately obvious. A brief comment would improve clarity.

📝 Suggested comment
+	// useKeyless=true is fine for verification since we use the certificate
+	// stored in the profile annotations, regardless of how the profile was signed
 	adapter, err := NewCosignAdapter(true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/verify.go` around lines 29 - 49, Add a short inline comment
next to the hardcoded useKeyless=true calls to NewCosignAdapter(true) and
NewCosignVerifier(true) explaining that verification uses the certificate
embedded in the annotations (so keyless=true is safe/intentional here), and
mention that signatures may come from either keyless or key-based signing but
the certificate in annotations is used for verification; place this comment
adjacent to the calls in VerifySignature (or the function containing adapter/
verifier creation) so future readers understand the rationale.
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)

249-264: Consider extracting verification logic into a helper method to reduce duplication.

The same verification pattern is repeated three times (lines 249-264, 336-349, and 567-587). This could be extracted into a helper like verifyProfileIfEnabled(profile *v1beta1.ApplicationProfile, name, namespace string) error.

♻️ Proposed helper method
+// verifyApplicationProfile verifies the profile signature if verification is enabled.
+// Returns error if verification fails, nil otherwise (including when verification is disabled).
+func (apc *ApplicationProfileCacheImpl) verifyApplicationProfile(profile *v1beta1.ApplicationProfile, context string) error {
+	if !apc.cfg.EnableProfileVerification {
+		return nil
+	}
+	profileAdapter := profiles.NewApplicationProfileAdapter(profile)
+	if err := signature.VerifyProfile(profileAdapter); err != nil {
+		logger.L().Warning(context+" verification failed, skipping",
+			helpers.String("profile", profile.Name),
+			helpers.String("namespace", profile.Namespace),
+			helpers.Error(err))
+		return err
+	}
+	logger.L().Debug(context+" verification successful",
+		helpers.String("profile", profile.Name),
+		helpers.String("namespace", profile.Namespace))
+	return nil
+}

Then replace each verification block with:

if err := apc.verifyApplicationProfile(fullProfile, "profile"); err != nil {
    continue // or return, depending on context
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around
lines 249 - 264, Extract the repeated signature verification block into a method
on applicationProfileCache (e.g., apc.verifyApplicationProfile) that checks
apc.cfg.EnableProfileVerification, constructs
profiles.NewApplicationProfileAdapter(fullProfile), calls
signature.VerifyProfile, logs the same Warning (including profile.Name,
namespace, workloadID when available) and Debug messages, and returns an error
when verification fails; then replace the three inline blocks that use
fullProfile/profile.Name/namespace/workloadID and signature.VerifyProfile with a
single call to apc.verifyApplicationProfile(...) and act on its error (continue
or return) as before.
pkg/signature/verify_test.go (1)

171-177: Test assertions are too weak - they only log but don't verify expected behavior.

The test checks if issuers/identities are the same and logs, but doesn't assert the expected difference between key-based (local) and keyless signing. Based on other tests, SignProfileWithKey should produce issuer local, while SignProfileKeyless produces a different issuer.

💚 Proposed fix with meaningful assertions
-	if sig1.Issuer == sig2.Issuer && sig1.Issuer != "" {
-		t.Log("Both profiles have same issuer")
-	}
-
-	if sig1.Identity == sig2.Identity && sig1.Identity != "" {
-		t.Log("Both profiles have same identity")
-	}
+	// Key-based signing should have "local" issuer
+	if sig1.Issuer != "local" {
+		t.Errorf("Expected key-based signing issuer 'local', got '%s'", sig1.Issuer)
+	}
+
+	// Keyless signing should have a non-local issuer (or at least be different in test env)
+	// Note: In test environment, keyless may also use "local" - adjust assertion as needed
+	if sig1.Identity != "local-key" {
+		t.Errorf("Expected key-based signing identity 'local-key', got '%s'", sig1.Identity)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/verify_test.go` around lines 171 - 177, The test currently only
logs when sig1.Issuer/sig1.Identity equals sig2.*; replace those logs with
concrete assertions: verify that the profile signed with SignProfileWithKey
(sig1) has Issuer == "local" and that the profile signed with SignProfileKeyless
(sig2) does not have Issuer == "local" (use t.Fatalf/t.Errorf to fail the test
on mismatch), and similarly assert expected differences for Identity (e.g.,
assert sig1.Identity is set to the local identity and sig2.Identity differs or
is empty per the keyless behavior). Update the checks referencing sig1, sig2,
Issuer, Identity, SignProfileWithKey, and SignProfileKeyless to assert
equality/inequality rather than just t.Log.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/sign-profile/main.go`:
- Around line 99-103: The CLI currently requires --key (keyFile) but never uses
it when calling signature.SignProfileWithKey; update main.go so that when
useKeyless is false you load the private key from keyFile (or extend
signature.SignProfileWithKey to accept a key path) and pass the loaded key or
keyFile into the signing call; specifically, locate the validation block using
keyFile/useKeyless and the call site of signature.SignProfileWithKey and either
(A) change the call to signature.SignProfileWithKey(..., keyFile) if the API is
extended, or (B) load/parse the private key from keyFile in main.go and pass the
resulting signer/crypto.Value into signature.SignProfileWithKey so the CLI --key
flag actually affects signing.
- Around line 106-111: parseVerifyFlags (and the similar
parseExtractSignatureFlags) never initialize the profileType flag, leaving
profileType empty so the downstream check (profileType != "auto") forces
explicit parsing and yields "unknown profile type"; fix by adding a StringVar
for profileType with a sensible default (e.g., "auto") in parseVerifyFlags and
the corresponding parseExtractSignatureFlags so profileType is initialized
before the check, ensuring the branch that handles auto-detection is taken when
no explicit type is passed.
- Around line 124-128: The generate-keypair command declares publicOnly
(parseGenerateFlags) but never uses it and always writes sig.Certificate as a
single blob; update the command to explicitly output separate public and private
key files and honor the publicOnly flag: add/recognize distinct outputs (e.g.,
--output-private and --output-public or derive public file as output + ".pub"),
change the key generation/emit code that currently writes sig.Certificate to
instead write the private key when not publicOnly and always write the public
key (or only the public when publicOnly is true), and ensure the code paths
reference the existing symbols publicOnly, outputFile (or new
outputPrivate/outputPublic vars) and the created keypair (the
sig/Certificate/privateKey values) so the produced files match the flag
semantics and the generate-keypair contract.
- Around line 41-43: The bug is that when you rewrite the inferred subcommand
variable command to "sign" because the first arg starts with "-", you still pass
os.Args[2:] into the flag parsing logic which drops that first flag; update the
argument selection so that when command was set to "sign" due to a leading flag
you parse os.Args[1:] (include the original first arg), otherwise continue
parsing os.Args[2:]. Locate the code that sets command and the subsequent flag
parsing (references: the command variable and the flag parsing calls that
currently use os.Args[2:]), introduce a small helper like argsForCommand (or
inline conditional) to choose os.Args[1:] when command was rewritten and
os.Args[2:] otherwise, and apply the same fix to the other similar sites noted
(the other occurrences around the same logic).

In `@docs/signing/README.md`:
- Around line 326-329: The example Kubernetes manifest uses an invalid container
name "app容器" under the spec -> containers -> name field; replace it with a
DNS-label-compliant name (lowercase alphanumeric and hyphens) such as
"app-container", ensuring the value for the containers.name field matches the
Kubernetes naming rules (^[a-z0-9]([-a-z0-9]*[a-z0-9])?$) and contains only
ASCII lowercase letters, digits, and hyphens.
- Around line 444-448: The ordered list item header "2. **Sign Before
Applying**" (the line that begins with that text) is incorrectly numbered
duplicate; change its leading "2." to "3." so the sequence after the previous
"2." item is correct; update only the numeric prefix of the "Sign Before
Applying" list item to "3." to fix the numbering.

In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 249-264: The profile verification block (guarded by
apc.cfg.EnableProfileVerification) currently skips on VerifyProfile error
without updating the existing profileState set earlier (see variable
profileState), causing the cache to show a successful state despite the profile
not being loaded; modify the error branch inside the signature.VerifyProfile
check to update profileState with the verification error and appropriate failed
status/completion fields (mirroring how user-defined profiles are handled on
verification failure), log the error, persist the updated profileState, and then
continue to the next profile to ensure consistent state reporting.

In `@pkg/signature/cosign_adapter.go`:
- Around line 106-113: The current ecdsaSign implementation builds the signature
by appending r.Bytes() and s.Bytes(), which strips leading zeros and breaks the
verifier that expects fixed 32-byte components; replace this with ASN.1 DER
encoding: add the "encoding/asn1" import, declare type ecdsaSignature struct {
R, S *big.Int }, change CosignAdapter.ecdsaSign to return
asn1.Marshal(ecdsaSignature{R: r, S: s}), and update the verification logic in
the same file (the code that currently slices the signature into fixed 32-byte
r/s) to use asn1.Unmarshal into ecdsaSignature and use the unmarshalled R and S
for verification.
- Around line 120-152: The code currently accepts an attacker-controlled public
key from sig.Certificate (used by cryptoutils.UnmarshalPEMToPublicKey) and only
checks sig.Issuer/sig.Identity when useKeyless is set, so fix by requiring and
validating an x509 certificate chain and trust anchor instead of directly
unmarshaling a raw public key: parse sig.Certificate as an x509.Certificate,
verify its chain/signature against configured trust roots (e.g., Fulcio root or
pinned CA) and certificate transparency bindings (Rekor/SCT) before extracting
the public key, ensure the extracted key matches the certificate public key used
for ecdsa.Verify, and replace the simple non-empty checks on
sig.Issuer/sig.Identity with authoritative checks that the certificate
SAN/subject matches expected pinned identities; if allowUntrusted is false,
reject any annotation-supplied/self-signed key material outright in the code
paths around cryptoutils.UnmarshalPEMToPublicKey, ecdsa.Verify, useKeyless,
sig.Issuer, and sig.Identity.

In `@pkg/signature/profiles/adapter_test.go`:
- Around line 274-278: The test currently ignores errors from
signature.SignProfileWithKey and signature.GetProfileSignature; change the calls
to capture their returned errors and fail the test if any occur (e.g., err !=
nil => t.Fatalf or require.NoError) before using apSig/spSig, and also assert
that apSig and spSig are non-nil/valid; update the calls to
signature.SignProfileWithKey(apAdapter) and
signature.SignProfileWithKey(spAdapter) and to
signature.GetProfileSignature(apAdapter)/GetProfileSignature(spAdapter) to check
and handle the returned errors immediately to avoid panics and misleading
assertions.

In `@pkg/signature/sign.go`:
- Around line 52-57: The current code replaces all profile annotations by
calling profile.SetAnnotations(annotations) with only the signature annotations
returned from adapter.EncodeSignatureToAnnotations(sig); instead, retrieve the
existing annotations (e.g., profile.GetAnnotations()), merge the signature
annotations into that map (overwriting keys from the new signature where
necessary) and then call profile.SetAnnotations(mergedAnnotations) so existing
metadata like kubescape.io/wlid and kubescape.io/status are preserved; locate
the EncodeSignatureToAnnotations call, the annotations variable, and the
profile.SetAnnotations invocation to implement this merge.

---

Nitpick comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 249-264: Extract the repeated signature verification block into a
method on applicationProfileCache (e.g., apc.verifyApplicationProfile) that
checks apc.cfg.EnableProfileVerification, constructs
profiles.NewApplicationProfileAdapter(fullProfile), calls
signature.VerifyProfile, logs the same Warning (including profile.Name,
namespace, workloadID when available) and Debug messages, and returns an error
when verification fails; then replace the three inline blocks that use
fullProfile/profile.Name/namespace/workloadID and signature.VerifyProfile with a
single call to apc.verifyApplicationProfile(...) and act on its error (continue
or return) as before.

In `@pkg/signature/sign_test.go`:
- Around line 170-175: The test case name "No annotations" is misleading because
NewMockSignableProfile creates an empty map (annotations:
make(map[string]string)); update the test to either rename the case to "Empty
annotations" to reflect the actual input, or change the fixture to construct a
MockSignableProfile with annotations set to nil (e.g.,
&MockSignableProfile{annotations: nil, ...}) so it exercises the nil-path in
DecodeSignatureFromAnnotations; modify the entry that currently calls
NewMockSignableProfile("uid","ns","name", map[string]string{}) to your chosen
fix and update the test name accordingly.

In `@pkg/signature/verify_test.go`:
- Around line 171-177: The test currently only logs when
sig1.Issuer/sig1.Identity equals sig2.*; replace those logs with concrete
assertions: verify that the profile signed with SignProfileWithKey (sig1) has
Issuer == "local" and that the profile signed with SignProfileKeyless (sig2)
does not have Issuer == "local" (use t.Fatalf/t.Errorf to fail the test on
mismatch), and similarly assert expected differences for Identity (e.g., assert
sig1.Identity is set to the local identity and sig2.Identity differs or is empty
per the keyless behavior). Update the checks referencing sig1, sig2, Issuer,
Identity, SignProfileWithKey, and SignProfileKeyless to assert
equality/inequality rather than just t.Log.

In `@pkg/signature/verify.go`:
- Around line 29-49: Add a short inline comment next to the hardcoded
useKeyless=true calls to NewCosignAdapter(true) and NewCosignVerifier(true)
explaining that verification uses the certificate embedded in the annotations
(so keyless=true is safe/intentional here), and mention that signatures may come
from either keyless or key-based signing but the certificate in annotations is
used for verification; place this comment adjacent to the calls in
VerifySignature (or the function containing adapter/ verifier creation) so
future readers understand the rationale.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a8e36e and 3599207.

📒 Files selected for processing (17)
  • cmd/sign-profile/main.go
  • docs/signing/README.md
  • go.mod
  • pkg/config/config.go
  • pkg/objectcache/applicationprofilecache/applicationprofilecache.go
  • pkg/signature/annotations.go
  • pkg/signature/cosign_adapter.go
  • pkg/signature/interface.go
  • pkg/signature/profiles/adapter_test.go
  • pkg/signature/profiles/applicationprofile_adapter.go
  • pkg/signature/profiles/seccompprofile_adapter.go
  • pkg/signature/sign.go
  • pkg/signature/sign_test.go
  • pkg/signature/signer.go
  • pkg/signature/verifier.go
  • pkg/signature/verify.go
  • pkg/signature/verify_test.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (3)
cmd/sign-profile/main.go (1)

48-53: ⚠️ Potential issue | 🔴 Critical

Default sign-profile --... still drops the first flag.

Line 87 always parses os.Args[2:]. When command is inferred (Line 43 path), the first real flag is at os.Args[1], so it gets skipped.

🐛 Proposed fix
-func parseSignFlags() {
+func parseSignFlags(args []string) {
 	fs := flag.NewFlagSet("sign-profile sign", flag.ExitOnError)
@@
-	if err := fs.Parse(os.Args[2:]); err != nil {
+	if err := fs.Parse(args); err != nil {
 		fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err)
 		os.Exit(1)
 	}
@@
 	switch command {
 	case "sign", "":
-		parseSignFlags()
-		if argsRewritten {
-			os.Args = append([]string{"sign-profile"}, os.Args[1:]...)
-		}
+		if argsRewritten {
+			parseSignFlags(os.Args[1:])
+		} else {
+			parseSignFlags(os.Args[2:])
+		}

Also applies to: 78-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/sign-profile/main.go` around lines 48 - 53, The bug is that when command
is inferred (switch case "sign",""), parseSignFlags() is called while os.Args
still contains the original argv, then later code unshifts "sign-profile"
(argsRewritten path) causing the first real flag to be dropped because parsing
uses os.Args[2:]; fix by ensuring the argv mutation happens before parsing or by
parsing the correct slice based on argsRewritten: move the os.Args =
append([]string{"sign-profile"}, os.Args[1:]...) line to occur before calling
parseSignFlags(), or change parseSignFlags() invocation to pass the correct args
(use os.Args[1:] when argsRewritten is false and os.Args[2:] when a command
token is present) so that parseSignFlags() sees the first flag whether command
was explicit or inferred; update references to parseSignFlags, argsRewritten,
and the switch handling accordingly.
docs/signing/README.md (1)

450-459: ⚠️ Potential issue | 🟡 Minor

Fix ordered-list numbering continuity in Best Practices.

Line 450 starts another 3. after Line 445 already used 3.. Please renumber this sequence to keep it consistent.

📝 Suggested fix
-3. **Version Your Profiles**
+4. **Version Your Profiles**
    - Include version in metadata
    - Old signatures become invalid on content changes
 
-4. **Key Management for Local Signing**
+5. **Key Management for Local Signing**
    - Store keys in secure locations (HSM, KMS)
    - Rotate keys regularly
    - Use read-only keys for verification
 
-5. **Audit Trail**
+6. **Audit Trail**
    - Store signing timestamps
    - Track who signed what
    - Use GitHub Actions for audit logs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/signing/README.md` around lines 450 - 459, The ordered list numbering in
the "Best Practices" section of docs/signing/README.md is inconsistent—after the
first "3." the subsequent list item "3. **Version Your Profiles**" should be
renumbered to continue the sequence; update the numeric markers for "Version
Your Profiles", "Key Management for Local Signing", and "Audit Trail" so they
follow the previous item (e.g., 4., 5., 6.) to restore correct ordered-list
continuity.
pkg/signature/cosign_adapter.go (1)

202-227: ⚠️ Potential issue | 🔴 Critical

Strict verification still accepts attacker-controlled certificate material.

In Line 215-223, strict mode only performs local certificate sanity checks (CA/CN/time) and does not verify certificate trust chains. Line 253-255 also treats sig.Issuer/sig.Identity as trusted if merely non-empty. This still allows self-consistent forged profile+cert+signature bundles to pass.

🔧 Suggested direction (trust-anchor + claim binding)
@@
 	if !allowUntrusted {
-		if cert.IsCA || cert.Subject.CommonName == "" {
-			return fmt.Errorf("invalid certificate: must not be CA and must have a valid subject")
-		}
-
-		if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
-			return fmt.Errorf("certificate is not valid at this time")
-		}
+		roots, err := x509.SystemCertPool() // preferably configured Fulcio/pinned roots
+		if err != nil || roots == nil {
+			return fmt.Errorf("failed to load trust roots: %w", err)
+		}
+		if _, err := cert.Verify(x509.VerifyOptions{
+			Roots:       roots,
+			CurrentTime: time.Now(),
+			KeyUsages:   []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+		}); err != nil {
+			return fmt.Errorf("certificate chain verification failed: %w", err)
+		}
+
+		if c.useKeyless {
+			if sig.Identity == "" || cert.Subject.CommonName != sig.Identity {
+				return fmt.Errorf("identity mismatch between signature metadata and certificate")
+			}
+			if sig.Issuer == "" || cert.Issuer.CommonName != sig.Issuer {
+				return fmt.Errorf("issuer mismatch between signature metadata and certificate")
+			}
+		}
 	}

Also applies to: 252-256

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 202 - 227, The current strict
path parses sig.Certificate but only runs local sanity checks (IsCA, CommonName,
dates) and later trusts sig.Issuer/sig.Identity if non-empty; fix by performing
a full x509 chain verification against a configured set of trust anchors and
intermediates (use x509.Verify with proper Roots and Intermediates and time
check) inside the block that handles sig.Certificate when allowUntrusted is
false, and require that x509.Verify succeeds; additionally, do not accept
sig.Issuer or sig.Identity as authoritative unless they are derived from the
verified certificate (e.g., compare sig.Issuer/sig.Identity to
cert.Subject/subjectAltName or parsed OID claims from cert) so only values bound
to a certificate that successfully verified are trusted.
🧹 Nitpick comments (2)
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)

252-258: Extract repeated “verification-failed” state updates into one helper.

These three blocks duplicate the same state-shaping logic. Centralizing it will reduce drift and keep status semantics consistent across paths.

Also applies to: 358-365, 589-596

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around
lines 252 - 258, Multiple code paths repeat creating an objectcache.ProfileState
with Completion:"failed", Status:"verification-failed", Name:profile.Name and
Error:err and then calling apc.workloadIDToProfileState.Set(workloadID,
profileState); factor this into a single helper on the receiver (e.g. func (apc
*ApplicationProfileCache) setVerificationFailed(workloadID string, profile
*ProfileType, err error)) that constructs the ProfileState and calls
apc.workloadIDToProfileState.Set; replace the three duplicated blocks (the
occurrences around the current diff and at lines ~358-365 and ~589-596) with
calls to that helper to keep status shaping consistent.
pkg/signature/cosign_adapter.go (1)

314-319: Use strict integer parsing for timestamp annotations.

fmt.Sscanf in Line 316 can accept partial numeric prefixes (e.g., "123abc"). strconv.ParseInt is stricter for annotation validation.

🔧 Suggested refactor
 import (
@@
 	"math/big"
+	"strconv"
 	"time"
@@
 	if timestamp, ok := annotations[AnnotationTimestamp]; ok {
-		var ts int64
-		_, err = fmt.Sscanf(timestamp, "%d", &ts)
+		ts, err := strconv.ParseInt(timestamp, 10, 64)
 		if err != nil {
 			return nil, fmt.Errorf("failed to parse timestamp: %w", err)
 		}
 		sig.Timestamp = ts
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 314 - 319, The code currently
uses fmt.Sscanf to parse the AnnotationTimestamp value (see AnnotationTimestamp
handling and the local variable ts), which allows partial numeric parsing;
replace that with strconv.ParseInt on the timestamp string (use base 10 and
64-bit) and assign the parsed int64 to ts, returning a wrapped error if ParseInt
fails to ensure strict integer validation of the annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/config.go`:
- Line 184: The config key enableProfileVerification set with viper.SetDefault
isn't bound to the documented ENABLE_PROFILE_VERIFICATION env var because
viper.AutomaticEnv() doesn't map camelCase to SCREAMING_SNAKE_CASE; update the
config initialization to explicitly bind the env var (e.g., call
viper.BindEnv("enableProfileVerification", "ENABLE_PROFILE_VERIFICATION")) or
configure an env key replacer (via viper.SetEnvKeyReplacer) before
viper.AutomaticEnv() so that the enableProfileVerification key correctly picks
up ENABLE_PROFILE_VERIFICATION from the environment.

In `@pkg/signature/cosign_adapter.go`:
- Around line 241-247: The code calling asn1.Unmarshal into ecdsaSignature
ignores the returned rest and doesn't validate R/S before calling ecdsa.Verify;
change the asn1.Unmarshal call to capture the rest value and return an error if
len(rest) != 0, and add checks that ecdsaSig.R and ecdsaSig.S are non-nil (and
return an error if they are) before invoking ecdsa.Verify with ecdsaPubKey,
digestBytes and ecdsaSig.R/S; this ensures strict DER parsing and avoids nil
dereferences.

In `@pkg/signature/profiles/applicationprofile_adapter.go`:
- Around line 18-23: GetAnnotations on ApplicationProfileAdapter returns a newly
created map when profile.Annotations is nil but does not persist it, so callers'
mutations are lost; change GetAnnotations to allocate a new map when
a.profile.Annotations == nil, assign it back to a.profile.Annotations (so the
profile holds the map) and then return it, referencing the GetAnnotations
method, the ApplicationProfileAdapter receiver a, the profile.Annotations field,
and the existing SetAnnotations behavior if needed for consistency.

In `@pkg/signature/profiles/seccompprofile_adapter.go`:
- Around line 18-23: GetAnnotations currently returns a newly allocated map when
s.profile.Annotations is nil but does not attach it to the adapter, so mutations
are lost; update SeccompProfileAdapter.GetAnnotations to initialize
s.profile.Annotations in-place (set s.profile.Annotations =
make(map[string]string) when nil) and then return s.profile.Annotations so
callers can safely mutate the returned map directly.

In `@pkg/signature/sign_test.go`:
- Around line 196-199: The test setup calls SignProfileKeyless(tt.profile) but
ignores its returned error; update the setup branch where tt.setupSign is true
to capture and assert the error so failures surface immediately (e.g., err :=
SignProfileKeyless(tt.profile) and fail the test on non-nil error using the test
helper in scope such as t.Fatalf/t.Fatalf-like assertion or require.NoError).
Ensure the alternative path with tt.setupAnnotations remains unchanged and use
the same t instance for error reporting.

In `@pkg/signature/sign.go`:
- Around line 11-35: SignProfile currently dereferences the incoming profile
(calls profile.GetContent()) without validating it, which panics on a nil input;
update SignProfile to validate that the profile argument is not nil at the start
and return a clear error (e.g., fmt.Errorf("nil profile")) instead of
proceeding, and also ensure any subsequent use of profile (GetContent) is only
after that nil-check; reference the SignProfile function and the
SignableProfile/GetContent call when making the change.

In `@pkg/signature/verifier.go`:
- Around line 18-24: The Verify and VerifyAllowUntrusted methods on
CosignVerifier dereference v.adapter without nil-checking; update both methods
(Verify and VerifyAllowUntrusted) to first verify that v and v.adapter are
non-nil and return a clear error (e.g., "uninitialized CosignVerifier adapter"
or similar) if they are nil, otherwise call v.adapter.VerifyData(data, sig,
...); reference CosignVerifier, Verify, VerifyAllowUntrusted, and
adapter.VerifyData when applying the change.

In `@pkg/signature/verify.go`:
- Around line 11-23: The VerifyProfile entrypoint assumes the incoming profile
is non-nil and can panic when calling profile.GetAnnotations(); add a nil guard
at the start of VerifyProfile to validate the profile argument (e.g., if profile
== nil) and return a clear error before proceeding, so subsequent calls like
profile.GetAnnotations() are safe; update any tests or callers if needed to
handle the new error path.

---

Duplicate comments:
In `@cmd/sign-profile/main.go`:
- Around line 48-53: The bug is that when command is inferred (switch case
"sign",""), parseSignFlags() is called while os.Args still contains the original
argv, then later code unshifts "sign-profile" (argsRewritten path) causing the
first real flag to be dropped because parsing uses os.Args[2:]; fix by ensuring
the argv mutation happens before parsing or by parsing the correct slice based
on argsRewritten: move the os.Args = append([]string{"sign-profile"},
os.Args[1:]...) line to occur before calling parseSignFlags(), or change
parseSignFlags() invocation to pass the correct args (use os.Args[1:] when
argsRewritten is false and os.Args[2:] when a command token is present) so that
parseSignFlags() sees the first flag whether command was explicit or inferred;
update references to parseSignFlags, argsRewritten, and the switch handling
accordingly.

In `@docs/signing/README.md`:
- Around line 450-459: The ordered list numbering in the "Best Practices"
section of docs/signing/README.md is inconsistent—after the first "3." the
subsequent list item "3. **Version Your Profiles**" should be renumbered to
continue the sequence; update the numeric markers for "Version Your Profiles",
"Key Management for Local Signing", and "Audit Trail" so they follow the
previous item (e.g., 4., 5., 6.) to restore correct ordered-list continuity.

In `@pkg/signature/cosign_adapter.go`:
- Around line 202-227: The current strict path parses sig.Certificate but only
runs local sanity checks (IsCA, CommonName, dates) and later trusts
sig.Issuer/sig.Identity if non-empty; fix by performing a full x509 chain
verification against a configured set of trust anchors and intermediates (use
x509.Verify with proper Roots and Intermediates and time check) inside the block
that handles sig.Certificate when allowUntrusted is false, and require that
x509.Verify succeeds; additionally, do not accept sig.Issuer or sig.Identity as
authoritative unless they are derived from the verified certificate (e.g.,
compare sig.Issuer/sig.Identity to cert.Subject/subjectAltName or parsed OID
claims from cert) so only values bound to a certificate that successfully
verified are trusted.

---

Nitpick comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 252-258: Multiple code paths repeat creating an
objectcache.ProfileState with Completion:"failed", Status:"verification-failed",
Name:profile.Name and Error:err and then calling
apc.workloadIDToProfileState.Set(workloadID, profileState); factor this into a
single helper on the receiver (e.g. func (apc *ApplicationProfileCache)
setVerificationFailed(workloadID string, profile *ProfileType, err error)) that
constructs the ProfileState and calls apc.workloadIDToProfileState.Set; replace
the three duplicated blocks (the occurrences around the current diff and at
lines ~358-365 and ~589-596) with calls to that helper to keep status shaping
consistent.

In `@pkg/signature/cosign_adapter.go`:
- Around line 314-319: The code currently uses fmt.Sscanf to parse the
AnnotationTimestamp value (see AnnotationTimestamp handling and the local
variable ts), which allows partial numeric parsing; replace that with
strconv.ParseInt on the timestamp string (use base 10 and 64-bit) and assign the
parsed int64 to ts, returning a wrapped error if ParseInt fails to ensure strict
integer validation of the annotation.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a8e36e and 322de50.

📒 Files selected for processing (17)
  • cmd/sign-profile/main.go
  • docs/signing/README.md
  • go.mod
  • pkg/config/config.go
  • pkg/objectcache/applicationprofilecache/applicationprofilecache.go
  • pkg/signature/annotations.go
  • pkg/signature/cosign_adapter.go
  • pkg/signature/interface.go
  • pkg/signature/profiles/adapter_test.go
  • pkg/signature/profiles/applicationprofile_adapter.go
  • pkg/signature/profiles/seccompprofile_adapter.go
  • pkg/signature/sign.go
  • pkg/signature/sign_test.go
  • pkg/signature/signer.go
  • pkg/signature/verifier.go
  • pkg/signature/verify.go
  • pkg/signature/verify_test.go

@matthyx
Copy link
Contributor Author

matthyx commented Mar 3, 2026

@coderabbitai please close all addressed comments.

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

@matthyx: I'll resolve all the addressed review comments on this PR.

✅ Actions performed

Comments resolved. Auto-approval is disabled; enable reviews.request_changes_workflow to approve automatically.

@matthyx matthyx marked this pull request as ready for review March 10, 2026 17:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)

370-387: ⚠️ Potential issue | 🔴 Critical

Re-verify the base profile before caching the merged result.

This path verifies fullUserProfile, then refetches originalProfile from storage and writes the merge back to workloadIDToProfile without verifying that second fetch. With verification enabled, a tampered base profile can still bypass the new integrity check whenever a user-managed overlay is present.

🛠️ Suggested fix
 	originalProfile, err := apc.storageClient.GetApplicationProfile(toMerge.profile.Namespace, toMerge.profile.Name)
 	if err != nil {
 		logger.L().Error("failed to get original profile",
 			helpers.String("namespace", toMerge.profile.Namespace),
 			helpers.String("profileName", toMerge.profile.Name),
 			helpers.Error(err))
 		return
 	}
+	if err := apc.verifyApplicationProfile(originalProfile, toMerge.wlid, "profile"); err != nil {
+		return
+	}
 	// Merge the profiles
 	mergedProfile := apc.performMerge(originalProfile, fullUserProfile)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around
lines 370 - 387, The code verifies fullUserProfile but does not re-verify the
freshly fetched base profile from storage, allowing a tampered base to bypass
integrity checks; after calling apc.storageClient.GetApplicationProfile (the
fetched originalProfile), call apc.verifyApplicationProfile(originalProfile,
toMerge.wlid, "base profile") and handle any error the same way as the earlier
verify (log and return) before calling apc.performMerge and writing the merged
result into workloadIDToProfile so both overlay and base are validated.
♻️ Duplicate comments (2)
pkg/signature/sign_test.go (1)

200-204: ⚠️ Potential issue | 🟡 Minor

Check signing setup error in test case.

The SignObjectKeyless call in the setup ignores the returned error. If signing fails during setup, the test will proceed with an unsigned profile, making downstream assertions misleading.

💚 Proposed fix
 		t.Run(tt.name, func(t *testing.T) {
 			if tt.setupSign {
-				SignObjectKeyless(tt.profile)
+				if err := SignObjectKeyless(tt.profile); err != nil {
+					t.Fatalf("SignObjectKeyless setup failed: %v", err)
+				}
 			} else if tt.setupAnnotations != nil {
 				tt.setupAnnotations(tt.profile)
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/sign_test.go` around lines 200 - 204, The test setup is
ignoring the error returned by SignObjectKeyless, which can leave the profile
unsigned and cause misleading failures; update the setup branch where
SignObjectKeyless(tt.profile) is called to capture its error and fail the test
on error (e.g., use t.Fatalf or require.NoError) so that SignObjectKeyless and
any errors are asserted, similar to how tt.setupAnnotations is invoked for
setupAnnotations(tt.profile).
pkg/signature/cosign_adapter.go (1)

287-370: ⚠️ Potential issue | 🟠 Major

Verification relies on self-signed certificates without external trust anchors.

This continues the concern from previous reviews: the verification accepts certificates and public keys from the signature annotations without validating against Fulcio roots or Rekor transparency logs. An attacker who can modify the profile can also replace the signature and certificate with their own self-signed material.

The allowUntrusted flag provides some distinction, but even "strict" mode (allowUntrusted=false) only validates:

  • Certificate is not CA (line 305)
  • Certificate is within validity period (lines 309-311)
  • Identity matches CN (lines 313-315)

It does not validate the certificate chain against Fulcio roots or verify Rekor bundle entries.

For production use with real security requirements, consider integrating with sigstore's verification infrastructure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 287 - 370, The VerifyData
function accepts certificates/public keys from the signature payload and only
checks basic properties (IsCA, validity window, CN) controlled by
allowUntrusted, which leaves verification vulnerable to self-signed or
attacker-provided certs; update VerifyData to perform real chain and
transparency verification: when allowUntrusted is false and c.useKeyless (or
when a certificate is present) validate the certificate chain against Fulcio
root(s) and, if present, verify the Rekor bundle/entry (or fail if missing)
using sigstore/cosign utilities (e.g., call sigstore trust root verification and
cosign.VerifyBundle or equivalent), ensure you reference sig.Certificate,
sig.RekorBundle (or add it to Signature), and the VerifyData code paths that
call sigstore_signature.LoadVerifier so the verifier is only created after
successful chain + Rekor verification.
🧹 Nitpick comments (6)
pkg/signature/profiles/seccompprofile_adapter.go (1)

40-59: Consider initializing nil Labels for consistent JSON serialization.

When s.profile.Labels is nil, the JSON output will include "labels": null, but when it's an empty map, it will be "labels": {}. This inconsistency between cluster-side and signing-side representations could cause hash mismatches during verification.

The ApplicationProfileAdapter has normalization logic for PolicyByRuleId maps; consider applying similar treatment here for Labels if consistency is required.

♻️ Optional fix for label consistency
 func (s *SeccompProfileAdapter) GetContent() interface{} {
 	apiVersion := s.profile.APIVersion
 	if apiVersion == "" {
 		apiVersion = "spdx.softwarecomposition.kubescape.io/v1beta1"
 	}
 	kind := s.profile.Kind
 	if kind == "" {
 		kind = "SeccompProfile"
 	}
+	labels := s.profile.Labels
+	if labels == nil {
+		labels = make(map[string]string)
+	}
 	return map[string]interface{}{
 		"apiVersion": apiVersion,
 		"kind":       kind,
 		"metadata": map[string]interface{}{
 			"name":      s.profile.Name,
 			"namespace": s.profile.Namespace,
-			"labels":    s.profile.Labels,
+			"labels":    labels,
 		},
 		"spec": s.profile.Spec,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/profiles/seccompprofile_adapter.go` around lines 40 - 59, The
GetContent method in SeccompProfileAdapter returns s.profile.Labels as-is which
yields "labels": null when nil and causes inconsistent JSON/hashing; update
SeccompProfileAdapter.GetContent to normalize labels by replacing a nil
s.profile.Labels with an empty map before building the returned map (i.e.,
ensure labels is a non-nil map[string]string when constructing the "metadata"
entry) so the JSON always serializes to "{}" for empty labels.
pkg/signature/cosign_adapter.go (3)

34-43: Prefer blank identifier imports over unused variable declarations.

These variables are declared but never used, serving only to ensure the packages are linked. This is unconventional and may trigger linter warnings. Use blank identifier imports instead.

♻️ Proposed refactor
-var _ = cosign.Signature
-var _ = providers.Enabled
-var _ = bundle.RekorBundle{}
-var _ = api.CertificateRequest{}
-var _ = client.Rekor{}
-var _ = models.LogEntry{}
-var _ = fulcioroots.Get
-var _ = oauthflow.OIDConnect
-var _ = oauthflow.DefaultIDTokenGetter

If these imports are needed for side effects, use blank identifier imports in the import block:

import (
	_ "github.com/sigstore/cosign/v2/pkg/cosign"
	// ... etc
)

If they're placeholders for future use, consider removing them until needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 34 - 43, Replace the unused
variable declarations (cosign.Signature, providers.Enabled, bundle.RekorBundle,
api.CertificateRequest, client.Rekor, models.LogEntry, fulcioroots.Get,
oauthflow.OIDConnect, oauthflow.DefaultIDTokenGetter) with proper blank
identifier imports in the package import block (e.g., import _ "module/path")
for any packages required only for side effects, or remove these placeholder
declarations entirely if the packages are not needed; update the import list to
include the corresponding packages with blank identifier imports instead of
keeping the var _ = ... lines so linters no longer flag unused variables.

217-246: Self-signed certificate has a one-year validity period.

The generated certificate is valid for 365 days (line 229). For keyless signing, certificates are typically short-lived (minutes) to reduce the window of key compromise. For key-based signing intended for offline/air-gapped environments, a longer validity may be acceptable.

Ensure this aligns with your security model and consider making the validity period configurable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 217 - 246, The
generateCertificate function currently sets a fixed 365-day validity; make the
certificate lifetime configurable to support short-lived (minutes) keyless certs
or longer offline certs: add a CertValidity (time.Duration) field or
configuration on the CosignAdapter and use it when computing NotAfter (replace
time.Now().Add(365*24*time.Hour) with time.Now().Add(c.CertValidity)), provide a
sensible default (e.g., 15m for keyless) when constructing CosignAdapter, and
ensure any callers or tests are updated to set/expect the configured duration.

416-436: Silent fallback from base64 to raw bytes may mask encoding issues.

When base64 decoding fails, the code silently falls back to treating the value as raw bytes (lines 420-421, 427-428, 435-436). This could mask legitimate encoding errors or allow malformed data to pass through.

Consider logging a warning when falling back, or failing explicitly if the annotation is expected to be base64-encoded.

♻️ Proposed fix to add logging on fallback
 	sig.Signature, err = base64.StdEncoding.DecodeString(signatureB64)
 	if err != nil {
-		// Try raw if base64 fails
+		// Try raw if base64 fails - this handles legacy or non-standard encodings
 		sig.Signature = []byte(signatureB64)
+		// Consider: logger.L().Debug("Signature annotation not base64 encoded, using raw bytes")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 416 - 436, The code silently
falls back to raw bytes when base64 decoding fails for sig.Signature,
sig.Certificate and sig.RekorBundle (base64.StdEncoding.DecodeString used
against annotations[AnnotationCertificate] and
annotations[AnnotationRekorBundle]), which can hide encoding problems; update
each decode branch to emit a warning (e.g., logger.Warnf or similar) when
DecodeString returns an error that includes the annotation key and the decode
error, and only fallback to raw bytes if configured to be tolerant (or
alternatively return the error to the caller for strict mode); ensure the
warning references sig.Signature, sig.Certificate and sig.RekorBundle so it’s
clear which field had the issue.
pkg/signature/profiles/applicationprofile_adapter.go (2)

67-76: Consider initializing nil Labels for consistent JSON serialization.

Same concern as SeccompProfileAdapter: when a.profile.Labels is nil, JSON renders "labels": null vs "labels": {} for empty maps, potentially causing hash mismatches during verification.

♻️ Optional fix for label consistency
+	labels := a.profile.Labels
+	if labels == nil {
+		labels = make(map[string]string)
+	}
 	return map[string]interface{}{
 		"apiVersion": apiVersion,
 		"kind":       kind,
 		"metadata": map[string]interface{}{
 			"name":      a.profile.Name,
 			"namespace": a.profile.Namespace,
-			"labels":    a.profile.Labels,
+			"labels":    labels,
 		},
 		"spec": a.profile.Spec,
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/profiles/applicationprofile_adapter.go` around lines 67 - 76,
The returned manifest may emit "labels": null when a.profile.Labels is nil,
causing inconsistent JSON/hashing; update the ApplicationProfileAdapter code
that builds the map (the block returning apiVersion/kind/metadata/spec) to
ensure labels is always a non-nil map by replacing "labels": a.profile.Labels
with a guarded value that returns an empty map (e.g., map[string]string{}) when
a.profile.Labels == nil, otherwise use a.profile.Labels so serialization yields
{} instead of null.

43-57: Normalization mutates the original profile object.

The PolicyByRuleId normalization directly modifies a.profile.Spec.Containers[i].PolicyByRuleId. While this ensures consistent JSON representation for signing, it has the side effect of mutating the caller's profile object. If the caller expects the profile to remain unchanged after calling GetContent(), this could lead to unexpected behavior.

Consider whether this mutation is intentional or if a defensive copy should be made.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/profiles/applicationprofile_adapter.go` around lines 43 - 57,
The normalization loop in GetContent (which sets PolicyByRuleId on
a.profile.Spec.Containers / InitContainers / EphemeralContainers) mutates the
original profile; instead make a defensive deep copy of the profile/spec before
normalizing so callers' objects are unchanged. Update the code in
applicationprofile_adapter.go to copy a.profile (or at least a.profile.Spec and
its Containers/InitContainers/EphemeralContainers and their PolicyByRuleId maps)
into a local variable, perform the nil->make map initialization on that copy,
and then use the copy for JSON/signing output (avoid changing a.profile in
place). Ensure copies include container slices and nested maps so no original
reference is modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/sign-object/main.go`:
- Around line 43-47: The code currently rewrites any leading-dash command into
"sign" (using variables command and argsRewritten), which causes "sign-object
-h" or "--help" to show sign subcommand help; adjust the conditional so it first
checks for explicit global help flags (e.g., if command == "-h" || command ==
"--help" || (len(os.Args)>1 && os.Args[1] == "-h") etc.) and returns/prints
top-level usage instead of rewriting, otherwise fall back to the existing if
strings.HasPrefix(command, "-") { command = "sign"; argsRewritten = true }
logic; update the handling around argsRewritten and any help-display code paths
to ensure global help is preserved.

In `@docs/signing/README.md`:
- Around line 255-260: Update the README entry for the --output and
--public-only flags to accurately describe the file layout: clarify that the CLI
writes the private key to the path given by --output and writes the public key
to the same path with a ".pub" suffix (unless --public-only is set, in which
case only the public file is written), and remove or replace the current
phrasing that implies both keys are written into a single PEM file; specifically
edit the table descriptions for `--output` and `--public-only` to reflect this
behavior so users know which file to use for signing.
- Around line 70-102: The Mermaid diagram block in README.md is plain text and
should be fenced so it renders and passes markdownlint; wrap the entire graph
(from the "Diagram" header through the closing triple backticks) in a fenced
code block starting with ```mermaid and ending with ``` so the graph (graph TB
... end) is treated as a mermaid diagram (look for the block containing "Signing
Flow", "Verification Flow", and the mermaid graph statements like A[K8s
Resource] --> B[Adapter]) and replace the current plain text with the fenced
mermaid block.

In `@go.mod`:
- Line 71: Update the modernc.org/sqlite dependency in go.mod from v1.40.1 to
v1.46.1 and refresh module files: change the version string for
modernc.org/sqlite to v1.46.1, then run the Go tooling (e.g., go get
modernc.org/sqlite@v1.46.1 and go mod tidy) to update go.sum and ensure builds
pass; verify imports and any API changes in code referencing modernc.org/sqlite
compile cleanly.
- Around line 49-52: The go.mod lists outdated sigstore modules; update the
module versions for github.com/sigstore/cosign/v2 -> v3.0.5,
github.com/sigstore/fulcio -> v1.8.5, and github.com/sigstore/rekor -> v1.5.1,
then run go mod tidy and the test suite to catch any breakage; note that
upgrading github.com/sigstore/cosign/v2 to v3.0.5 is a major-version bump that
may require API changes—search usages of the cosign package imports/usages to
adjust call sites accordingly and fix any compile/test failures.

In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 592-595: The code currently bails out of addContainer when
verifyApplicationProfile(fullProfile, workloadID, "user-defined profile")
returns an error, which prevents the later containerIDToInfo.Set and thus
updateAllProfiles from seeing the container; change this to handle a failed
optional/user-defined profile by logging the verification error (or recording it
on the container entry) and continuing execution so that containerIDToInfo.Set
is still called; keep the existing behavior for required profile verification
but do not return from addContainer on user-defined profile verify
failures—ensure verifyApplicationProfile is only fatal for required profiles and
non-fatal for the "user-defined profile" case.

In `@pkg/rulemanager/ruleswatcher/watcher.go`:
- Around line 149-152: The verifyRules method reads
w.cfg.EnableSignatureVerification without guarding for a nil cfg; update
verifyRules to treat a nil w.cfg as verification disabled (i.e., return nil if
w.cfg == nil || !w.cfg.EnableSignatureVerification) or alternatively normalize
w.cfg in the constructor (NewRulesWatcher) to a default config with
EnableSignatureVerification=false so verifyRules can safely access the flag;
reference the verifyRules method and NewRulesWatcher to locate where to
implement the guard/normalization.
- Around line 99-105: The verifyRules(rules) call already logs verification
failures, so remove the duplicate logger.L().Warning(...) block inside the
RulesWatcher loop: when verifyRules returns an error, simply increment
skippedVerificationCount and continue without emitting another warning; keep the
verifyRules function as the sole logger for signature failures and ensure only
skippedVerificationCount++ and continue remain in the error branch where
verifyRules is invoked.
- Around line 155-156: Replace the brittle string comparison against err.Error()
in the watcher (the isMissingSig check) with an errors.Is() check against a
sentinel error exported from pkg/signature (e.g., signature.ErrObjectNotSigned);
update pkg/signature to export that sentinel (or a helper like
signature.IsObjectNotSigned(err)) and have VerifyObject return or wrap that
sentinel when the annotation is missing, then in watcher.go use errors.Is(err,
signature.ErrObjectNotSigned) (or call signature.IsObjectNotSigned) to classify
unsigned objects instead of fmt.Sprintf string matching.

In `@pkg/signature/cluster_flow_test.go`:
- Around line 86-98: The test currently ignores the certificate path because
generateTestCertificate does not use privKey and the test calls
verifier.VerifySignature using privKey.PublicKey directly; update the test to
derive a real x509 certificate from privKey (or have generateTestCertificate
accept and use privKey) and store its base64 in the
"signature.kubescape.io/certificate" annotation, then decode via
cosignAdapter.DecodeSignatureFromAnnotations and either construct the verifier
from the parsed certificate (not privKey.PublicKey) or exercise the higher-level
flow by calling VerifyObject or CosignVerifier so the code path that
extracts/parses/verifies the certificate from annotations is exercised; ensure
references to certBytes, sig, and the annotated values are used for verification
rather than privKey.PublicKey.
- Around line 82-104: The test currently ignores errors and only logs
verification results; update it to fail immediately on any error and assert
verification succeeds: check and handle returned errors from ecdsa.GenerateKey,
sigstore_signature.LoadECDSASigner, signer.SignMessage, generateTestCertificate
(if it can error), cosignAdapter.DecodeSignatureFromAnnotations,
sigstore_signature.LoadECDSAVerifier and hex.DecodeString, and replace t.Logf
calls around verifier.VerifySignature with assertions (e.g.,
t.Fatalf/t.Fatalf-style failure) that the VerifySignature calls on verifier (for
both bytes.NewReader([]byte(hash)) and bytes.NewReader(hashBytes)) return nil
(success); ensure each step uses the specific symbols shown (GenerateKey,
LoadECDSASigner, SignMessage, DecodeSignatureFromAnnotations, LoadECDSAVerifier,
VerifySignature) so failures stop the test immediately.

In `@pkg/signature/profiles/rules_adapter.go`:
- Around line 17-21: The GetAnnotations method currently mutates r.rules by
allocating r.rules.Annotations when nil; remove that mutation and simply return
the annotations as-is (i.e., return r.rules.Annotations without creating a new
map) so read-only callers (e.g., VerifyObject) do not change unsigned Rules;
ensure any code that needs to write or persist annotations initializes the map
in the write path (where mutations are expected) instead of here.

---

Outside diff comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 370-387: The code verifies fullUserProfile but does not re-verify
the freshly fetched base profile from storage, allowing a tampered base to
bypass integrity checks; after calling apc.storageClient.GetApplicationProfile
(the fetched originalProfile), call
apc.verifyApplicationProfile(originalProfile, toMerge.wlid, "base profile") and
handle any error the same way as the earlier verify (log and return) before
calling apc.performMerge and writing the merged result into workloadIDToProfile
so both overlay and base are validated.

---

Duplicate comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 287-370: The VerifyData function accepts certificates/public keys
from the signature payload and only checks basic properties (IsCA, validity
window, CN) controlled by allowUntrusted, which leaves verification vulnerable
to self-signed or attacker-provided certs; update VerifyData to perform real
chain and transparency verification: when allowUntrusted is false and
c.useKeyless (or when a certificate is present) validate the certificate chain
against Fulcio root(s) and, if present, verify the Rekor bundle/entry (or fail
if missing) using sigstore/cosign utilities (e.g., call sigstore trust root
verification and cosign.VerifyBundle or equivalent), ensure you reference
sig.Certificate, sig.RekorBundle (or add it to Signature), and the VerifyData
code paths that call sigstore_signature.LoadVerifier so the verifier is only
created after successful chain + Rekor verification.

In `@pkg/signature/sign_test.go`:
- Around line 200-204: The test setup is ignoring the error returned by
SignObjectKeyless, which can leave the profile unsigned and cause misleading
failures; update the setup branch where SignObjectKeyless(tt.profile) is called
to capture its error and fail the test on error (e.g., use t.Fatalf or
require.NoError) so that SignObjectKeyless and any errors are asserted, similar
to how tt.setupAnnotations is invoked for setupAnnotations(tt.profile).

---

Nitpick comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 34-43: Replace the unused variable declarations (cosign.Signature,
providers.Enabled, bundle.RekorBundle, api.CertificateRequest, client.Rekor,
models.LogEntry, fulcioroots.Get, oauthflow.OIDConnect,
oauthflow.DefaultIDTokenGetter) with proper blank identifier imports in the
package import block (e.g., import _ "module/path") for any packages required
only for side effects, or remove these placeholder declarations entirely if the
packages are not needed; update the import list to include the corresponding
packages with blank identifier imports instead of keeping the var _ = ... lines
so linters no longer flag unused variables.
- Around line 217-246: The generateCertificate function currently sets a fixed
365-day validity; make the certificate lifetime configurable to support
short-lived (minutes) keyless certs or longer offline certs: add a CertValidity
(time.Duration) field or configuration on the CosignAdapter and use it when
computing NotAfter (replace time.Now().Add(365*24*time.Hour) with
time.Now().Add(c.CertValidity)), provide a sensible default (e.g., 15m for
keyless) when constructing CosignAdapter, and ensure any callers or tests are
updated to set/expect the configured duration.
- Around line 416-436: The code silently falls back to raw bytes when base64
decoding fails for sig.Signature, sig.Certificate and sig.RekorBundle
(base64.StdEncoding.DecodeString used against annotations[AnnotationCertificate]
and annotations[AnnotationRekorBundle]), which can hide encoding problems;
update each decode branch to emit a warning (e.g., logger.Warnf or similar) when
DecodeString returns an error that includes the annotation key and the decode
error, and only fallback to raw bytes if configured to be tolerant (or
alternatively return the error to the caller for strict mode); ensure the
warning references sig.Signature, sig.Certificate and sig.RekorBundle so it’s
clear which field had the issue.

In `@pkg/signature/profiles/applicationprofile_adapter.go`:
- Around line 67-76: The returned manifest may emit "labels": null when
a.profile.Labels is nil, causing inconsistent JSON/hashing; update the
ApplicationProfileAdapter code that builds the map (the block returning
apiVersion/kind/metadata/spec) to ensure labels is always a non-nil map by
replacing "labels": a.profile.Labels with a guarded value that returns an empty
map (e.g., map[string]string{}) when a.profile.Labels == nil, otherwise use
a.profile.Labels so serialization yields {} instead of null.
- Around line 43-57: The normalization loop in GetContent (which sets
PolicyByRuleId on a.profile.Spec.Containers / InitContainers /
EphemeralContainers) mutates the original profile; instead make a defensive deep
copy of the profile/spec before normalizing so callers' objects are unchanged.
Update the code in applicationprofile_adapter.go to copy a.profile (or at least
a.profile.Spec and its Containers/InitContainers/EphemeralContainers and their
PolicyByRuleId maps) into a local variable, perform the nil->make map
initialization on that copy, and then use the copy for JSON/signing output
(avoid changing a.profile in place). Ensure copies include container slices and
nested maps so no original reference is modified.

In `@pkg/signature/profiles/seccompprofile_adapter.go`:
- Around line 40-59: The GetContent method in SeccompProfileAdapter returns
s.profile.Labels as-is which yields "labels": null when nil and causes
inconsistent JSON/hashing; update SeccompProfileAdapter.GetContent to normalize
labels by replacing a nil s.profile.Labels with an empty map before building the
returned map (i.e., ensure labels is a non-nil map[string]string when
constructing the "metadata" entry) so the JSON always serializes to "{}" for
empty labels.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e772b00d-4a8d-464d-8b32-21513c9e0a1c

📥 Commits

Reviewing files that changed from the base of the PR and between 1a8e36e and 411ed7b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (25)
  • cmd/main.go
  • cmd/sign-object/main.go
  • docs/signing/README.md
  • go.mod
  • pkg/config/config.go
  • pkg/objectcache/applicationprofilecache/applicationprofilecache.go
  • pkg/rulemanager/ruleswatcher/watcher.go
  • pkg/signature/annotations.go
  • pkg/signature/cluster_flow_test.go
  • pkg/signature/cluster_scenario_test.go
  • pkg/signature/cosign_adapter.go
  • pkg/signature/cosign_adapter_test.go
  • pkg/signature/interface.go
  • pkg/signature/profiles/adapter_test.go
  • pkg/signature/profiles/applicationprofile_adapter.go
  • pkg/signature/profiles/empty_typemeta_test.go
  • pkg/signature/profiles/rules_adapter.go
  • pkg/signature/profiles/rules_adapter_test.go
  • pkg/signature/profiles/seccompprofile_adapter.go
  • pkg/signature/sign.go
  • pkg/signature/sign_test.go
  • pkg/signature/signer.go
  • pkg/signature/verifier.go
  • pkg/signature/verify.go
  • pkg/signature/verify_test.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
cmd/sign-object/main.go (1)

43-47: ⚠️ Potential issue | 🟡 Minor

Global help flags still get rewritten to sign.

Running sign-object -h or sign-object --help matches the strings.HasPrefix(command, "-") check before reaching the explicit help cases in the switch, causing users to see sign-specific help instead of global usage.

🐛 Proposed fix
 	command = os.Args[1]
 
 	argsRewritten := false
+	if command == "-h" || command == "--help" {
+		printUsage()
+		os.Exit(0)
+	}
 	if strings.HasPrefix(command, "-") {
 		command = "sign"
 		argsRewritten = true
 	}

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/sign-object/main.go` around lines 43 - 47, The current rewrite logic sets
command = "sign" when strings.HasPrefix(command, "-"), which incorrectly catches
global help flags like "-h" and "--help"; change the condition so help flags are
exempt (e.g., check for "-h" and "--help" first or require
strings.HasPrefix(command, "-") && command != "-h" && command != "--help") so
argsRewritten is only set for real subcommand shortcuts while preserving the
explicit help cases in the existing switch that handle "-h"/"--help".
docs/signing/README.md (2)

72-104: ⚠️ Potential issue | 🟡 Minor

Add the missing opening mermaid fence.

This block still starts as plain text and only has the closing fence, which matches the markdownlint failures on Line 89 and Line 104.

Suggested fix
 ### Diagram
+```mermaid
 graph TB
     subgraph "Signing Flow"
         A[K8s Resource] --> B[Adapter]
@@
 
     L --> M
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/signing/README.md around lines 72 - 104, Add the missing opening fenced
code block marker for the Mermaid diagram by inserting an opening "mermaid" fence immediately before the "graph TB" line so the block is recognized as Mermaid (the block already contains the closing ""); ensure there is no extra
indentation and the opening fence matches the existing closing fence to resolve
the markdownlint errors around the diagram section.


</details>

---

`257-263`: _⚠️ Potential issue_ | _🟡 Minor_

**Fix the `generate-keypair` output description.**

The table still implies both keys are written into one PEM file. The CLI writes the private key to `--output` and the public key to `--output + ".pub"`; `--public-only` should say that only the public file is emitted.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@docs/signing/README.md` around lines 257 - 263, Update the generate-keypair
flags documentation to accurately describe where each key is written: change the
`--output` description to state it writes the private key to the specified PEM
file and that the public key is written to `--output + ".pub"`, and change the
`--public-only` description to state it emits only the public key file (i.e.
only `--output + ".pub"`) rather than implying a combined PEM; reference the
`--output` and `--public-only` flags in the README table for these exact wording
changes.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>cmd/sign-object/main.go (2)</summary><blockquote>

`52-63`: **Remove dead code: `os.Args` modifications after parsing.**

These `os.Args` assignments occur after `parse*Flags()` functions have already completed parsing. They have no effect on the program behavior.

<details>
<summary>♻️ Proposed cleanup</summary>

```diff
 	case "sign", "":
 		parseSignFlags()
-		if argsRewritten {
-			os.Args = append([]string{"sign-object"}, os.Args[1:]...)
-		}
 	case "verify":
 		parseVerifyFlags()
-		os.Args = append([]string{"sign-object verify"}, os.Args[2:]...)
 	case "generate-keypair":
 		parseGenerateFlags()
-		os.Args = append([]string{"sign-object generate-keypair"}, os.Args[2:]...)
 	case "extract-signature":
 		parseExtractFlags()
-		os.Args = append([]string{"sign-object extract-signature"}, os.Args[2:]...)
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@cmd/sign-object/main.go` around lines 52 - 63, Remove the dead os.Args
reassignments that occur after flag parsing: the append calls modifying os.Args
in the branches for "verify", "generate-keypair", and "extract-signature" (and
the argsRewritten handling) have no effect because parseVerifyFlags(),
parseGenerateFlags(), and parseExtractFlags() already completed parsing; delete
those os.Args = append(...) lines (and any unused argsRewritten-related branch
if it is now unused) to clean up dead code while keeping the parse*Flags() calls
intact.
```

</details>

---

`387-391`: **Certificate type display may silently fail for non-PEM certificates.**

The signature annotations store certificates as base64-encoded raw bytes. `pem.Decode` returns `nil` for raw DER-encoded certificates, silently skipping the certificate type display. Consider parsing as DER if PEM decode fails.

<details>
<summary>♻️ Suggested improvement</summary>

```diff
 		block, _ := pem.Decode(sig.Certificate)
 		if block != nil {
 			fmt.Printf("  Certificate Type: %s\n", block.Type)
+		} else if len(sig.Certificate) > 0 {
+			// Try parsing as DER
+			cert, err := x509.ParseCertificate(sig.Certificate)
+			if err == nil {
+				fmt.Printf("  Certificate Subject: %s\n", cert.Subject.CommonName)
+			}
 		}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@cmd/sign-object/main.go` around lines 387 - 391, The code currently uses
pem.Decode(sig.Certificate) and only prints block.Type when block != nil, which
silently skips DER/raw certificates; update the logic in the certificate
handling (around pem.Decode and printing of certificate type) to attempt DER
parsing when pem.Decode returns nil: first try pem.Decode(sig.Certificate) and
print block.Type if present, otherwise try parsing sig.Certificate as a DER x509
certificate (using x509.ParseCertificate) and on success print an appropriate
type string (e.g., "X.509 DER") or the certificate's Subject/Issuer; ensure
error handling/logging for failed parses so non-PEM certificates don't silently
skip type output.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go:

  • Around line 327-340: The current code verifies the post-merge object
    (mergedNN) using signature.VerifyObjectStrict after performMerge, but
    performMerge mutates content while retaining original annotations so
    verification will incorrectly fail for valid user overrides; instead, when
    nnc.cfg.EnableSignatureVerification is true, call signature.VerifyObjectStrict
    on the fetched source objects (originalNN and fullUserNN) before performMerge
    (using profiles.NewNetworkNeighborhoodAdapter on each) OR, if mergedNN must be
    cacheable, re-sign the merged result after performMerge (update mergedNN
    annotations and call the signing API) and then use VerifyObjectStrict on the
    re-signed mergedNN; update the code paths around performMerge,
    NewNetworkNeighborhoodAdapter, and VerifyObjectStrict accordingly so signature
    checks validate the unmodified source or a freshly-signed merged object.
  • Around line 243-254: The signature-verification branch currently logs and
    continues without updating workloadIDToProfileState, causing callers of
    GetNetworkNeighborhoodState to miss the failure; modify the block inside
    nnc.cfg.EnableSignatureVerification (where adapter :=
    profiles.NewNetworkNeighborhoodAdapter(fullNN) and
    signature.VerifyObjectStrict(adapter) is called) to persist the verification
    failure into workloadIDToProfileState: create or update the ProfileState entry
    for workloadID (and namespace/name as appropriate) to mark it failed/errored and
    include the verification error (err.Error()) and any relevant metadata before
    continuing, so the state reflects the rejected profile.

Duplicate comments:
In @cmd/sign-object/main.go:

  • Around line 43-47: The current rewrite logic sets command = "sign" when
    strings.HasPrefix(command, "-"), which incorrectly catches global help flags
    like "-h" and "--help"; change the condition so help flags are exempt (e.g.,
    check for "-h" and "--help" first or require strings.HasPrefix(command, "-") &&
    command != "-h" && command != "--help") so argsRewritten is only set for real
    subcommand shortcuts while preserving the explicit help cases in the existing
    switch that handle "-h"/"--help".

In @docs/signing/README.md:

  • Around line 72-104: Add the missing opening fenced code block marker for the
    Mermaid diagram by inserting an opening "mermaid" fence immediately before the "graph TB" line so the block is recognized as Mermaid (the block already contains the closing ""); ensure there is no extra indentation and the
    opening fence matches the existing closing fence to resolve the markdownlint
    errors around the diagram section.
  • Around line 257-263: Update the generate-keypair flags documentation to
    accurately describe where each key is written: change the --output description
    to state it writes the private key to the specified PEM file and that the public
    key is written to --output + ".pub", and change the --public-only
    description to state it emits only the public key file (i.e. only --output + ".pub") rather than implying a combined PEM; reference the --output and
    --public-only flags in the README table for these exact wording changes.

Nitpick comments:
In @cmd/sign-object/main.go:

  • Around line 52-63: Remove the dead os.Args reassignments that occur after flag
    parsing: the append calls modifying os.Args in the branches for "verify",
    "generate-keypair", and "extract-signature" (and the argsRewritten handling)
    have no effect because parseVerifyFlags(), parseGenerateFlags(), and
    parseExtractFlags() already completed parsing; delete those os.Args =
    append(...) lines (and any unused argsRewritten-related branch if it is now
    unused) to clean up dead code while keeping the parse*Flags() calls intact.
  • Around line 387-391: The code currently uses pem.Decode(sig.Certificate) and
    only prints block.Type when block != nil, which silently skips DER/raw
    certificates; update the logic in the certificate handling (around pem.Decode
    and printing of certificate type) to attempt DER parsing when pem.Decode returns
    nil: first try pem.Decode(sig.Certificate) and print block.Type if present,
    otherwise try parsing sig.Certificate as a DER x509 certificate (using
    x509.ParseCertificate) and on success print an appropriate type string (e.g.,
    "X.509 DER") or the certificate's Subject/Issuer; ensure error handling/logging
    for failed parses so non-PEM certificates don't silently skip type output.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `2d6ebf82-5539-4d16-9058-e53243fbe4fb`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 411ed7b076e8d520b45ac3a6e8309898be137423 and 479285c088fb78f1e1af7ad599f91f87d4fc27c9.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `cmd/sign-object/main.go`
* `docs/signing/README.md`
* `pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

matthyx and others added 18 commits March 11, 2026 17:46
Implement generic signing and verification framework for ApplicationProfiles
and SeccompProfiles using ECDSA P-256 signatures compatible with cosign.

Key features:
- SignableProfile interface for extensible profile type support
- Keyless signing support using OIDC identity providers
- Key-based signing for offline/air-gapped environments
- Signature storage in profile metadata.annotations
- CLI tool (sign-profile) for signing and verifying profiles
- Automatic verification in ApplicationProfileCache on profile load

Core implementation:
- pkg/signature/ package with ECDSA P-256 signing/verification
- Profile adapters for ApplicationProfile and SeccompProfile
- EnableProfileVerification config flag (default: false)
- On verification failure: skip profile with warning (don't crash)

Files:
- pkg/signature/annotations.go, interface.go, sign.go, verify.go
- pkg/signature/cosign_adapter.go, signer.go, verifier.go
- pkg/signature/profiles/applicationprofile_adapter.go
- pkg/signature/profiles/seccompprofile_adapter.go
- cmd/sign-profile/main.go
- pkg/config/config.go - add EnableProfileVerification flag
- pkg/objectcache/applicationprofilecache/ - integrate verification at load time
- docs/signing/README.md - comprehensive documentation

Tests:
- 15 signature package tests covering signing, verification, tampering detection
- 5 adapter tests for ApplicationProfile and SeccompProfile adapters
- All tests passing

Co-authored-by: Cerebras Agent <193945191+isaact-cerebras@users.noreply.github.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Co-authored-by: Cerebras Agent <193945191+isaact-cerebras@users.noreply.github.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
- Implement RulesAdapter implementing SignableProfile interface
- Add signature verification in RulesWatcher when enabled
- Pass config to RulesWatcher to enable profile verification toggle
- Add comprehensive tests for rules signing and verification
- Track skipped rules due to verification failures in logs
- Maintain consistent behavior with application and seccomp profiles

Co-authored-by: Cerebras Agent <193945191+isaact-cerebras@users.noreply.github.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go (1)

329-373: ⚠️ Potential issue | 🟠 Major

Drop stale signature annotations before caching the merged profile.

performMerge changes the signed content, but mergedNN still inherits the original signature metadata from DeepCopy(). That leaves a mutated object in cache that still looks signed, and any downstream re-verification will fail against stale annotations.

🧹 Proposed fix
 	// Merge the network neighborhoods
 	mergedNN := nnc.performMerge(originalNN, fullUserNN)
+	for _, key := range []string{
+		signature.AnnotationSignature,
+		signature.AnnotationCertificate,
+		signature.AnnotationRekorBundle,
+		signature.AnnotationIssuer,
+		signature.AnnotationIdentity,
+		signature.AnnotationTimestamp,
+	} {
+		delete(mergedNN.Annotations, key)
+	}
 
 	// Update the cache with the merged network neighborhood
 	nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go` around
lines 329 - 373, The merged network neighborhood returned by
nnc.performMerge(originalNN, fullUserNN) still carries signature-related
annotations from the DeepCopy of original objects; before calling
nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN) remove/clear the
stale signature metadata (i.e., any annotation keys the
signature.VerifyObjectStrict/Sign logic uses) from mergedNN.Annotations so the
cached object no longer appears signed and won't trigger false re-verification
failures.
pkg/signature/cosign_adapter.go (1)

334-397: ⚠️ Potential issue | 🔴 Critical

allowUntrusted=false still trusts key material from the object itself.

The strict path accepts a raw PUBLIC KEY PEM from annotations, and even the CERTIFICATE branch never chains the leaf to a trusted Fulcio root or validates Rekor/CT material. A user who can rewrite the object can therefore replace both the content and verification key and still make VerifyObjectStrict pass. Sigstore's keyless verification requires the certificate chain plus Rekor/CT evidence, not just an embedded PEM blob. (docs.sigstore.dev)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 334 - 397, The current strict
verification path (in the pem.Decode branch handling "CERTIFICATE" and "PUBLIC
KEY" inside VerifyObjectStrict) accepts an embedded PUBLIC KEY PEM and does not
validate a certificate chain or Rekor/CT evidence, allowing object authors to
substitute keys; fix by forbidding or treating embedded public keys as untrusted
when allowUntrusted==false and by requiring and validating a proper x509 chain
and SignedEntryTimestamp/Rekor proof for Fulcio-issued certificates: parse the
certificate chain (not just the leaf) from sig.Certificate, verify it against
the Fulcio root set and system roots, validate certificate validity period and
key usage, and run Sigstore/Cosign Rekor/CT verification APIs (the library
verification that checks SignedEntryTimestamp/Rekor proof) before calling
sigstore_signature.LoadVerifier; ensure any fallback PUBLIC KEY handling is only
allowed when allowUntrusted==true.
🧹 Nitpick comments (2)
pkg/signature/profiles/networkneighborhood_adapter_test.go (1)

11-65: Add coverage for the empty-TypeMeta path.

This test only exercises the branch where APIVersion and Kind are already populated. GetContent() has separate defaulting logic for empty TypeMeta, and those fields are part of the canonical hash used by signing/verification, so a regression there would not be caught here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/profiles/networkneighborhood_adapter_test.go` around lines 11 -
65, Add a variant in TestNetworkNeighborhoodAdapter that constructs a
v1beta1.NetworkNeighborhood with an empty TypeMeta (no Kind/APIVersion), pass it
through NewNetworkNeighborhoodAdapter and call adapter.GetContent(), then assert
the returned content map contains the defaulted "kind" == "NetworkNeighborhood"
and "apiVersion" == "softwarecomposition.kubescape.io/v1beta1" (and validate
metadata.name/namespace and spec container content as in the existing
assertions); this ensures GetContent()'s defaulting path for empty TypeMeta is
covered and included in signing/verification hashing.
pkg/signature/cluster_flow_test.go (1)

104-112: Exercise the package-level annotation flow in this test.

The fixture hand-writes only signature and certificate annotations and then verifies with cert.PublicKey directly, so it won't catch regressions in EncodeSignatureToAnnotations, issuer/identity/timestamp handling, or VerifyObject*. Building the annotations via EncodeSignatureToAnnotations and asserting VerifyObjectAllowUntrusted(adapter) here would cover the flow the rest of the package actually uses.

Also applies to: 117-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cluster_flow_test.go` around lines 104 - 112, The test
currently hand-sets "signature" and "certificate" annotations and decodes via
cosignAdapter.DecodeSignatureFromAnnotations; instead exercise the package-level
annotation flow by using cosignAdapter.EncodeSignatureToAnnotations (or
EncodeSignatureToAnnotations) to produce the annotations from sig and certBytes,
apply them to adapter via adapter.SetAnnotations, then call
cosignAdapter.VerifyObjectAllowUntrusted(adapter) (or
VerifyObjectAllowUntrusted) and assert no error; this ensures
EncodeSignatureToAnnotations, issuer/identity/timestamp handling, and
VerifyObject* are validated rather than bypassed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 279-318: The code currently flips the entire workload state to
"verification-failed" inside verifyApplicationProfile, which wrongly affects the
whole workload when a user-managed overlay is rejected; change
verifyApplicationProfile to accept a recordFailure bool (e.g.,
verifyApplicationProfile(profile, workloadID, context, recordFailure)) and only
call apc.setVerificationFailed(workloadID, profile.Name, err) when recordFailure
is true; update callers so the normal profile load path passes
recordFailure=true and handleUserManagedProfile (and the user-overlay path)
passes recordFailure=false, and ensure any other direct calls (the code around
the existing setVerificationFailed usage) are adjusted likewise.
- Around line 592-605: The verifyApplicationProfile(...) failure branch
currently logs the error but then continues and overwrites the profile state,
allowing an invalid user-defined profile to be loaded; instead, after calling
verifyApplicationProfile(fullProfile, workloadID, "user-defined profile") and
detecting an error, return/skip further processing for that userDefinedProfile
so it is not applied, and do not call apc.workloadIDToProfileState.Set(...) with
a new ProfileState that wipes fields set by setVerificationFailed(); preserve or
rely on setVerificationFailed() to record the failure (or update only its Error
field if necessary) and ensure subsequent code that applies the profile checks
the verification result before loading.

In `@pkg/signature/cosign_adapter.go`:
- Around line 129-136: The provider-enabled branch currently hard-codes identity
and issuer after calling providers.Provide; instead parse the returned OIDC
token (tok) the same way the interactive/fallback flow does (use the existing
token parsing routine or JWT/OIDC parser used elsewhere to produce oidcToken)
and set identity = oidcToken.Subject and issuer = oidcToken.Issuer (remove the
hard-coded sigstoreOIDC/sigstoreIssuer assignments). Ensure you reuse the same
parsing/validation path used by the non-CI flow so the subject/issuer come from
the token claims.
- Around line 348-350: The certificate validity check currently uses time.Now()
and should instead use the signature's signing time: replace
time.Now().Before(cert.NotBefore) / time.Now().After(cert.NotAfter) with checks
against time.Unix(sig.Timestamp, 0) (i.e., use sig.Timestamp from the Signature
struct) so the Fulcio certificate is validated at signing time; update the logic
around cert.NotBefore and cert.NotAfter in the function that receives the
Signature (sig) to compare against time.Unix(sig.Timestamp, 0) rather than
time.Now().

In `@pkg/signature/verify.go`:
- Around line 22-28: The code currently returns a generic fmt.Errorf when
obj.GetAnnotations() is nil which prevents callers from detecting unsigned
objects via errors.Is; change the nil-annotations branch in verify.go to return
the sentinel ErrObjectNotSigned instead (or wrap it with %w if extra context is
desired) so that the check for AnnotationSignature and callers using
errors.Is(err, ErrObjectNotSigned) behave consistently; update the block around
GetAnnotations(), AnnotationSignature and ErrObjectNotSigned accordingly.

---

Duplicate comments:
In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`:
- Around line 329-373: The merged network neighborhood returned by
nnc.performMerge(originalNN, fullUserNN) still carries signature-related
annotations from the DeepCopy of original objects; before calling
nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN) remove/clear the
stale signature metadata (i.e., any annotation keys the
signature.VerifyObjectStrict/Sign logic uses) from mergedNN.Annotations so the
cached object no longer appears signed and won't trigger false re-verification
failures.

In `@pkg/signature/cosign_adapter.go`:
- Around line 334-397: The current strict verification path (in the pem.Decode
branch handling "CERTIFICATE" and "PUBLIC KEY" inside VerifyObjectStrict)
accepts an embedded PUBLIC KEY PEM and does not validate a certificate chain or
Rekor/CT evidence, allowing object authors to substitute keys; fix by forbidding
or treating embedded public keys as untrusted when allowUntrusted==false and by
requiring and validating a proper x509 chain and SignedEntryTimestamp/Rekor
proof for Fulcio-issued certificates: parse the certificate chain (not just the
leaf) from sig.Certificate, verify it against the Fulcio root set and system
roots, validate certificate validity period and key usage, and run
Sigstore/Cosign Rekor/CT verification APIs (the library verification that checks
SignedEntryTimestamp/Rekor proof) before calling
sigstore_signature.LoadVerifier; ensure any fallback PUBLIC KEY handling is only
allowed when allowUntrusted==true.

---

Nitpick comments:
In `@pkg/signature/cluster_flow_test.go`:
- Around line 104-112: The test currently hand-sets "signature" and
"certificate" annotations and decodes via
cosignAdapter.DecodeSignatureFromAnnotations; instead exercise the package-level
annotation flow by using cosignAdapter.EncodeSignatureToAnnotations (or
EncodeSignatureToAnnotations) to produce the annotations from sig and certBytes,
apply them to adapter via adapter.SetAnnotations, then call
cosignAdapter.VerifyObjectAllowUntrusted(adapter) (or
VerifyObjectAllowUntrusted) and assert no error; this ensures
EncodeSignatureToAnnotations, issuer/identity/timestamp handling, and
VerifyObject* are validated rather than bypassed.

In `@pkg/signature/profiles/networkneighborhood_adapter_test.go`:
- Around line 11-65: Add a variant in TestNetworkNeighborhoodAdapter that
constructs a v1beta1.NetworkNeighborhood with an empty TypeMeta (no
Kind/APIVersion), pass it through NewNetworkNeighborhoodAdapter and call
adapter.GetContent(), then assert the returned content map contains the
defaulted "kind" == "NetworkNeighborhood" and "apiVersion" ==
"softwarecomposition.kubescape.io/v1beta1" (and validate metadata.name/namespace
and spec container content as in the existing assertions); this ensures
GetContent()'s defaulting path for empty TypeMeta is covered and included in
signing/verification hashing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70d92705-634a-49d6-b279-4dc77f2bb1ce

📥 Commits

Reviewing files that changed from the base of the PR and between 479285c and 63429f9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • cmd/sign-object/main.go
  • docs/signing/README.md
  • go.mod
  • pkg/objectcache/applicationprofilecache/applicationprofilecache.go
  • pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go
  • pkg/rulemanager/ruleswatcher/watcher.go
  • pkg/signature/annotations.go
  • pkg/signature/cluster_flow_test.go
  • pkg/signature/cosign_adapter.go
  • pkg/signature/profiles/networkneighborhood_adapter.go
  • pkg/signature/profiles/networkneighborhood_adapter_test.go
  • pkg/signature/profiles/rules_adapter.go
  • pkg/signature/verify.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/signature/annotations.go
  • docs/signing/README.md

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)

594-617: ⚠️ Potential issue | 🔴 Critical

User-defined profile is loaded into cache even when verification fails.

When signature verification fails for a user-defined profile (lines 596-608), the code logs a warning and updates the profile state with an error, but then falls through to load the profile into the cache at line 611. This contradicts the stated requirement to skip profiles on verification failure and could allow untrusted profiles to be used.

🐛 Proposed fix
 				// Verify signature if enabled
 				if err := apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile"); err != nil {
 					logger.L().Warning("user-defined profile signature verification failed, continuing without signature",
 						helpers.String("containerID", containerID),
 						helpers.String("workloadID", workloadID),
 						helpers.String("namespace", container.K8s.Namespace),
 						helpers.String("profileName", userDefinedProfile),
 						helpers.Error(err))
-					// Update the profile state to indicate an error
-					profileState := &objectcache.ProfileState{
-						Error: fmt.Errorf("signature verification failed: %w", err),
-					}
-					apc.workloadIDToProfileState.Set(workloadID, profileState)
-				}
-
-				// Update the profile in the cache
-				apc.workloadIDToProfile.Set(workloadID, fullProfile)
-				logger.L().Debug("added user-defined profile to cache",
-					helpers.String("containerID", containerID),
-					helpers.String("workloadID", workloadID),
-					helpers.String("namespace", container.K8s.Namespace),
-					helpers.String("profileName", userDefinedProfile))
+					// verifyApplicationProfile already sets the profile state via setVerificationFailed
+					// Skip loading the profile into cache
+				} else {
+					// Update the profile in the cache only on successful verification
+					apc.workloadIDToProfile.Set(workloadID, fullProfile)
+					logger.L().Debug("added user-defined profile to cache",
+						helpers.String("containerID", containerID),
+						helpers.String("workloadID", workloadID),
+						helpers.String("namespace", container.K8s.Namespace),
+						helpers.String("profileName", userDefinedProfile))
+				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around
lines 594 - 617, The code currently logs and records an error when
apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile")
fails but still proceeds to cache the unverified profile via
apc.workloadIDToProfile.Set(workloadID, fullProfile); modify the control flow so
that if verifyApplicationProfile returns an error you do not call
apc.workloadIDToProfile.Set and instead skip adding fullProfile to the cache
(e.g., return/continue or otherwise exit the enclosing block after setting
workloadIDToProfileState), preserving the existing logging and profileState
update logic.
♻️ Duplicate comments (3)
pkg/signature/cosign_adapter.go (3)

343-397: ⚠️ Potential issue | 🔴 Critical

allowUntrusted=false still accepts attacker-supplied leaf certs.

The strict path parses the certificate bundled in the annotations and immediately uses cert.PublicKey, but it never verifies that certificate against any configured trust root or pinned key set. A modified object can be re-signed with a fresh self-signed leaf cert and still pass "strict" verification.

#!/bin/bash
set -euo pipefail

sed -n '343,397p' pkg/signature/cosign_adapter.go
printf '\n=== certificate-chain verification hooks ===\n'
rg -n 'cert\.Verify|x509\.VerifyOptions|x509\.CertPool|fulcioroots\.Get|VerifySignedCertificateTimestamp' pkg/signature/cosign_adapter.go

Expected result: the strict path loads cert.PublicKey directly and does not perform certificate-chain/root validation before trusting it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 343 - 397, The strict path
currently trusts the leaf certificate in the annotation (used when
allowUntrusted is false) without performing certificate-chain or root
verification; update the code around the block that calls
sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256) (and the
alternative public-key path using cryptoutils.UnmarshalPEMToPublicKey) to first
build and verify the x509 chain: create x509.VerifyOptions with a root pool
(e.g., fulcioroots.Get or configured CertPool), any intermediate pool, and
appropriate DNS/Time checks, call cert.Verify(VerifyOptions), and only on
successful verification proceed to LoadVerifier; if verification fails return an
explicit error about untrusted certificate chain.

348-350: ⚠️ Potential issue | 🟠 Major

Validate certificate lifetime at signing time, not wall-clock time.

Using time.Now() makes valid keyless signatures start failing as soon as the short-lived Fulcio cert expires. Compare sig.Timestamp to NotBefore/NotAfter so historical verification still works.

Proposed fix
-				if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
+				signedAt := time.Unix(sig.Timestamp, 0)
+				if signedAt.Before(cert.NotBefore) || signedAt.After(cert.NotAfter) {
 					return fmt.Errorf("certificate is not valid at this time")
 				}
When verifying Sigstore/Fulcio keyless signatures, should certificate validity be checked against the signing timestamp instead of the verifier's current wall-clock time?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 348 - 350, The certificate
validity check uses time.Now(); change it to validate against the signature's
signing time by comparing cert.NotBefore and cert.NotAfter to the signature
timestamp (sig.Timestamp) instead of wall-clock time. Convert sig.Timestamp into
a time.Time (e.g., time.Unix(sig.Timestamp, 0) or use the existing timestamp
type) and replace the two time.Now().Before/After comparisons with comparisons
against that signing time so historical verifications succeed; update the
condition around cert, sig.Timestamp, and the error return accordingly.

129-150: ⚠️ Potential issue | 🟠 Major

Keyless signing can't depend on browser auth or hard-coded claims.

The provider branch records fixed identity/issuer values, and the fallback opens an interactive OIDC flow from library code. That's already breaking CI, and it decouples the stored signer metadata from the actual token that Fulcio authenticated. Make the token source injectable/non-interactive and derive identity/issuer from the token claims.

How does Sigstore/Fulcio keyless signing derive signer identity and issuer from an OIDC token, and is it valid to hard-code those fields or require an interactive browser fallback in headless CI?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 129 - 150, The code currently
uses providers.Enabled/Provide to get tok but then hard-codes identity/issuer to
sigstoreOIDC/sigstoreIssuer and falls back to oauthflow.OIDConnect (interactive)
when no provider is enabled; replace this by making the token source injectable
(accept a non-interactive token provider or token string from context/params
instead of calling oauthflow.OIDConnect), stop using sigstoreOIDC/sigstoreIssuer
constants, and parse/validate the returned OIDC token (the tok value) to extract
claims for Subject and Issuer to populate identity and issuer; ensure functions
that call this code (or the constructor) accept a TokenProvider or raw token so
CI/headless environments can supply tokens without browser auth and the signer
metadata is derived from the token claims rather than hard-coded values.
🧹 Nitpick comments (1)
pkg/signature/sign.go (1)

77-79: SignObjectWithKey is misleading.

This helper only disables keyless mode; when no WithPrivateKey option is passed, NewCosignAdapter(false) generates a new keypair. For an offline-signing API, either accept a private key here or rename the helper to match what it actually does.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/sign.go` around lines 77 - 79, The helper SignObjectWithKey is
misleading because it only sets WithKeyless(false) and still allows
NewCosignAdapter(false) to generate a new keypair; fix by changing the helper to
accept a private key parameter and pass it through to SignObject, e.g. change
func SignObjectWithKey(obj SignableObject) to func SignObjectWithKey(obj
SignableObject, privateKey string) error and call SignObject(obj,
WithKeyless(false), WithPrivateKey(privateKey)); alternatively, if you prefer
not to change the API, rename the function to reflect that it only disables
keyless mode (e.g., SignObjectDisableKeyless) and update all callers and tests
to match the new signature/name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 284-298: Replace the fragile string comparison of err.Error() in
the signature verification block (where signature.VerifyObject(profileAdapter)
is called) with errors.Is(err, signature.ErrObjectNotSigned) to detect a missing
signature reliably; update the conditional to use errors.Is and keep the same
Debug log for missing signatures and Warning (including helpers.Error(err)) for
other errors; ensure the package imports the standard errors package so
errors.Is is available.

In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`:
- Around line 329-367: The merged NetworkNeighborhood must not retain signature
annotations from originalNN/fullUserNN; modify the merge path (e.g., in
performMerge or immediately after creating the mergedNN before any cache/store
call such as where nnc.workloadIDToProfileState.Set is invoked) to remove all
signature-related annotations (the keys used by signature.VerifyObjectStrict /
the signer: e.g., signature, signer public key, signing timestamp annotations)
from the merged object (or re-sign it) so downstream GetNetworkNeighborhood
cannot be mistaken for a valid signed object; ensure this stripping happens
after merge and before caching/storage.

In `@pkg/signature/cosign_adapter.go`:
- Around line 326-335: The VerifyData function currently dereferences
sig.Certificate and may panic if sig is nil; add an explicit nil-check at the
start of CosignAdapter.VerifyData (before calling pem.Decode) to return a
descriptive error when sig == nil, and likewise validate that sig.Certificate is
non-nil/empty and return an error instead of proceeding; update any error
messages to reference the Signature value to aid debugging (use the VerifyData
function name in the message).

In `@pkg/signature/profiles/adapter_test.go`:
- Around line 100-144: The test TestApplicationProfileAdapterSignAndVerify calls
signature.SignObjectKeyless which triggers an interactive OIDC flow and breaks
CI; either make the test skip in CI by checking os.Getenv("CI") and calling
t.Skipf(...) before invoking SignObjectKeyless, or change the test to use a
non-interactive path: replace signature.SignObjectKeyless(adapter) with
signature.SignObjectWithKey(adapter, testPrivateKey) using a generated in-test
key/fixture, then keep the subsequent checks and
signature.VerifyObjectStrict(adapter); update imports and add a small in-test
key generation helper if you choose the SignObjectWithKey route.
- Around line 232-274: TestSeccompProfileAdapterSignAndVerify currently calls
signature.SignObjectKeyless which is incompatible with CI; change the test to
perform a local/signing-key-based sign instead of keyless (e.g., replace
signature.SignObjectKeyless with the project’s local signing helper such as
SignObjectWithKey or a test signer/mocked signer) so the test can run offline,
keeping the rest of the flow (NewSeccompProfileAdapter, checking
profile.Annotations and signature.AnnotationSignature, and calling
signature.VerifyObjectStrict) unchanged; ensure the chosen signer produces the
same annotation key expected by VerifyObjectStrict.

In `@pkg/signature/profiles/rules_adapter_test.go`:
- Around line 117-120: The tests call signature.SignObjectKeyless(adapter) which
forces an interactive OIDC/browser flow; switch them to the non-interactive
keyed path by calling the keyed signing helper (e.g., signature.SignObject or
signature.SignObjectKeyed) with an ephemeral/test private key instead of
SignObjectKeyless, creating the test key inside the test (or reusing an existing
test helper) and passing it along with adapter; apply the same replacement for
both occurrences (the SignObjectKeyless calls at the two test sites) so the
adapter behavior is validated without invoking OIDC.

In `@pkg/signature/sign_test.go`:
- Around line 53-90: TestSignObjectKeyless triggers an interactive OIDC flow;
update the test to avoid interactive auth in CI by either skipping it when
running in CI (check env like CI or GITHUB_ACTIONS) or by injecting/mocking the
OIDC flow used by SignObjectKeyless so it returns a non-interactive,
deterministic response; modify TestSignObjectKeyless to detect CI and call
t.Skipf(...) or refactor the test to use a mock OIDC client/provider when
exercising SignObjectKeyless so the test runs headlessly and reliably in CI.

In `@pkg/signature/sign.go`:
- Around line 85-107: Both public helpers don't check for a nil SignableObject
and will panic; add a nil guard at the top of GetObjectSignature and IsSigned.
In GetObjectSignature (function GetObjectSignature) return a descriptive error
(e.g. "nil object") if obj == nil before calling obj.GetAnnotations(); in
IsSigned (function IsSigned) return false if obj == nil before accessing
GetAnnotations(); keep the existing annotations nil checks and existing behavior
for AnnotationSignature lookup.

---

Outside diff comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 594-617: The code currently logs and records an error when
apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile")
fails but still proceeds to cache the unverified profile via
apc.workloadIDToProfile.Set(workloadID, fullProfile); modify the control flow so
that if verifyApplicationProfile returns an error you do not call
apc.workloadIDToProfile.Set and instead skip adding fullProfile to the cache
(e.g., return/continue or otherwise exit the enclosing block after setting
workloadIDToProfileState), preserving the existing logging and profileState
update logic.

---

Duplicate comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 343-397: The strict path currently trusts the leaf certificate in
the annotation (used when allowUntrusted is false) without performing
certificate-chain or root verification; update the code around the block that
calls sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256) (and the
alternative public-key path using cryptoutils.UnmarshalPEMToPublicKey) to first
build and verify the x509 chain: create x509.VerifyOptions with a root pool
(e.g., fulcioroots.Get or configured CertPool), any intermediate pool, and
appropriate DNS/Time checks, call cert.Verify(VerifyOptions), and only on
successful verification proceed to LoadVerifier; if verification fails return an
explicit error about untrusted certificate chain.
- Around line 348-350: The certificate validity check uses time.Now(); change it
to validate against the signature's signing time by comparing cert.NotBefore and
cert.NotAfter to the signature timestamp (sig.Timestamp) instead of wall-clock
time. Convert sig.Timestamp into a time.Time (e.g., time.Unix(sig.Timestamp, 0)
or use the existing timestamp type) and replace the two time.Now().Before/After
comparisons with comparisons against that signing time so historical
verifications succeed; update the condition around cert, sig.Timestamp, and the
error return accordingly.
- Around line 129-150: The code currently uses providers.Enabled/Provide to get
tok but then hard-codes identity/issuer to sigstoreOIDC/sigstoreIssuer and falls
back to oauthflow.OIDConnect (interactive) when no provider is enabled; replace
this by making the token source injectable (accept a non-interactive token
provider or token string from context/params instead of calling
oauthflow.OIDConnect), stop using sigstoreOIDC/sigstoreIssuer constants, and
parse/validate the returned OIDC token (the tok value) to extract claims for
Subject and Issuer to populate identity and issuer; ensure functions that call
this code (or the constructor) accept a TokenProvider or raw token so
CI/headless environments can supply tokens without browser auth and the signer
metadata is derived from the token claims rather than hard-coded values.

---

Nitpick comments:
In `@pkg/signature/sign.go`:
- Around line 77-79: The helper SignObjectWithKey is misleading because it only
sets WithKeyless(false) and still allows NewCosignAdapter(false) to generate a
new keypair; fix by changing the helper to accept a private key parameter and
pass it through to SignObject, e.g. change func SignObjectWithKey(obj
SignableObject) to func SignObjectWithKey(obj SignableObject, privateKey string)
error and call SignObject(obj, WithKeyless(false), WithPrivateKey(privateKey));
alternatively, if you prefer not to change the API, rename the function to
reflect that it only disables keyless mode (e.g., SignObjectDisableKeyless) and
update all callers and tests to match the new signature/name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86cd8613-f9a2-4577-b445-8ce2e443d7cf

📥 Commits

Reviewing files that changed from the base of the PR and between 63429f9 and 9887527.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (28)
  • cmd/main.go
  • cmd/sign-object/main.go
  • docs/signing/README.md
  • go.mod
  • pkg/config/config.go
  • pkg/objectcache/applicationprofilecache/applicationprofilecache.go
  • pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go
  • pkg/rulemanager/ruleswatcher/watcher.go
  • pkg/signature/annotations.go
  • pkg/signature/cluster_flow_test.go
  • pkg/signature/cluster_scenario_test.go
  • pkg/signature/cosign_adapter.go
  • pkg/signature/cosign_adapter_test.go
  • pkg/signature/interface.go
  • pkg/signature/profiles/adapter_test.go
  • pkg/signature/profiles/applicationprofile_adapter.go
  • pkg/signature/profiles/empty_typemeta_test.go
  • pkg/signature/profiles/networkneighborhood_adapter.go
  • pkg/signature/profiles/networkneighborhood_adapter_test.go
  • pkg/signature/profiles/rules_adapter.go
  • pkg/signature/profiles/rules_adapter_test.go
  • pkg/signature/profiles/seccompprofile_adapter.go
  • pkg/signature/sign.go
  • pkg/signature/sign_test.go
  • pkg/signature/signer.go
  • pkg/signature/verifier.go
  • pkg/signature/verify.go
  • pkg/signature/verify_test.go
✅ Files skipped from review due to trivial changes (1)
  • docs/signing/README.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • pkg/signature/profiles/networkneighborhood_adapter_test.go
  • cmd/main.go
  • pkg/signature/signer.go
  • pkg/config/config.go
  • pkg/signature/verify_test.go
  • pkg/signature/cosign_adapter_test.go
  • pkg/signature/profiles/rules_adapter.go
  • pkg/signature/profiles/networkneighborhood_adapter.go

Comment on lines +117 to +120
err := signature.SignObjectKeyless(adapter)
if err != nil {
t.Fatalf("SignObjectKeyless failed: %v", err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep these adapter tests off the interactive OIDC path.

signature.SignObjectKeyless makes these unit tests depend on the browser/OIDC flow, so they fail in CI before they actually validate the adapter behavior. Use the non-interactive keyed path here, and keep any real keyless coverage behind an explicit integration-test gate.

Suggested change
-	err := signature.SignObjectKeyless(adapter)
+	err := signature.SignObjectWithKey(adapter)

Apply the same swap in both tests.

Also applies to: 169-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/profiles/rules_adapter_test.go` around lines 117 - 120, The
tests call signature.SignObjectKeyless(adapter) which forces an interactive
OIDC/browser flow; switch them to the non-interactive keyed path by calling the
keyed signing helper (e.g., signature.SignObject or signature.SignObjectKeyed)
with an ephemeral/test private key instead of SignObjectKeyless, creating the
test key inside the test (or reusing an existing test helper) and passing it
along with adapter; apply the same replacement for both occurrences (the
SignObjectKeyless calls at the two test sites) so the adapter behavior is
validated without invoking OIDC.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
pkg/signature/cosign_adapter.go (2)

331-340: ⚠️ Potential issue | 🔴 Critical

Guard VerifyData against nil sig before dereferencing it.

A nil sig panics at Line 339 (pem.Decode(sig.Certificate)) instead of failing closed. That turns malformed decode paths into a crash in the verification pipeline.

🐛 Proposed fix
 func (c *CosignAdapter) VerifyData(data []byte, sig *Signature, allowUntrusted bool) error {
+	if sig == nil {
+		return fmt.Errorf("VerifyData: signature is nil")
+	}
+
 	var verifier sigstore_signature.Verifier
 	var err error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 331 - 340, The VerifyData
function risks a panic when sig is nil because it dereferences sig to call
pem.Decode(sig.Certificate); add a nil-check at the start of VerifyData (in the
CosignAdapter.VerifyData function) to return a clear error if sig == nil before
any use of sig, and ensure downstream code (including the pem.Decode call and
any uses of sig.Certificate or sig.PublicKey) only executes when sig is non-nil.

129-141: ⚠️ Potential issue | 🟠 Major

Derive the signer identity from the returned OIDC token, not constants.

Lines 140-141 still hard-code sigstoreOIDC / sigstoreIssuer after providers.Provide(...). Fulcio binds the certificate to the token’s real claims, and VerifyData later matches sig.Identity against that certificate. This can stamp incorrect metadata and make provider-path signatures fail verification for any non-matching subject/issuer.

For Sigstore keyless signing with github.com/sigstore/cosign/v3/pkg/providers, should the signer identity and issuer be derived from the returned OIDC token claims instead of hard-coding them? Please point to the official Sigstore/Fulcio guidance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signature/cosign_adapter.go` around lines 129 - 141, After calling
providers.Provide(ctx, "sigstore") you must extract the actual OIDC token claims
from the returned token and set identity and issuer from those claims instead of
assigning the constants sigstoreOIDC/sigstoreIssuer; update the logic around
providers.Provide -> tok (or the returned token structure) to decode the token
claims (subject/sub and issuer/iss) and assign them to identity and issuer so
that VerifyData and sig.Identity will match the Fulcio-bound certificate; modify
the code paths that currently reference sigstoreOIDC and sigstoreIssuer to use
the parsed claims from the token returned by providers.Provide and ensure any
JWT/ID token parsing uses the token type returned by providers.Provide (or a
simple JWT parser if necessary) to extract "sub" and "iss".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 596-600: The verification failure path currently returns nil
without updating the profile state; update the code in the block that calls
apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile",
false) so that on error you set the profileState.Error (and any other relevant
fields like profileState.State) with the verification error before returning the
profileState (mirroring the fetch-failure handling). Ensure you reference and
update the same profileState variable used elsewhere in this function so
verification failures are recorded consistently for alerts/creator.go.

In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`:
- Around line 329-367: The current signature-failure branches set
workloadIDToProfileState and return but leave workloadIDToNetworkNeighborhood
containing a stale merged profile; when VerifyObjectStrict fails for the
originalNN (base-profile) you must evict nnc.workloadIDToNetworkNeighborhood for
toMerge.wlid (delete/Unset) before returning, and when VerifyObjectStrict fails
for fullUserNN (user-managed) after originalNN already verified you must restore
nnc.workloadIDToNetworkNeighborhood for toMerge.wlid to the originalNN value
(not the stale merged result) before returning; update the branches around
profiles.NewNetworkNeighborhoodAdapter/ signature.VerifyObjectStrict and use
workloadIDToNetworkNeighborhood.Set/Unset (or equivalent) referencing
toMerge.wlid, originalNN and fullUserNN to implement these fixes.

In `@pkg/signature/cosign_adapter.go`:
- Around line 388-394: The current check incorrectly ties Rekor bundle
enforcement to the adapter flag c.useKeyless; change the logic so strict
verification requires a Rekor bundle based on the signature's provenance not
adapter config — remove the dependency on c.useKeyless in the block that
inspects sig.RekorBundle and instead always treat a missing RekorBundle as a
strict-verification failure (or derive enforcement from the verification
request), so keyed signatures produced by signWithKey (which do not set
sig.RekorBundle) can succeed when allowed; update the conditional around
sig.RekorBundle (and any related error message) in the method containing that
logic and ensure NewCosignAdapter/NewCosignVerifier flows respect signature
provenance rather than c.useKeyless.

---

Duplicate comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 331-340: The VerifyData function risks a panic when sig is nil
because it dereferences sig to call pem.Decode(sig.Certificate); add a nil-check
at the start of VerifyData (in the CosignAdapter.VerifyData function) to return
a clear error if sig == nil before any use of sig, and ensure downstream code
(including the pem.Decode call and any uses of sig.Certificate or sig.PublicKey)
only executes when sig is non-nil.
- Around line 129-141: After calling providers.Provide(ctx, "sigstore") you must
extract the actual OIDC token claims from the returned token and set identity
and issuer from those claims instead of assigning the constants
sigstoreOIDC/sigstoreIssuer; update the logic around providers.Provide -> tok
(or the returned token structure) to decode the token claims (subject/sub and
issuer/iss) and assign them to identity and issuer so that VerifyData and
sig.Identity will match the Fulcio-bound certificate; modify the code paths that
currently reference sigstoreOIDC and sigstoreIssuer to use the parsed claims
from the token returned by providers.Provide and ensure any JWT/ID token parsing
uses the token type returned by providers.Provide (or a simple JWT parser if
necessary) to extract "sub" and "iss".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6dbbf2b2-67d6-4939-af7b-9a442f427f20

📥 Commits

Reviewing files that changed from the base of the PR and between 9887527 and fb7519d.

📒 Files selected for processing (6)
  • pkg/objectcache/applicationprofilecache/applicationprofilecache.go
  • pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go
  • pkg/signature/cluster_flow_test.go
  • pkg/signature/cosign_adapter.go
  • pkg/signature/profiles/networkneighborhood_adapter_test.go
  • pkg/signature/verify.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/signature/cluster_flow_test.go
  • pkg/signature/profiles/networkneighborhood_adapter_test.go
  • pkg/signature/verify.go

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
@matthyx matthyx requested a review from slashben March 11, 2026 21:47
@matthyx matthyx moved this to WIP in KS PRs tracking Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: WIP

Development

Successfully merging this pull request may close these issues.

1 participant